diff --git a/edc-web/app/__init__.py b/edc-web/app/__init__.py index 3e99325..49eb4a6 100644 --- a/edc-web/app/__init__.py +++ b/edc-web/app/__init__.py @@ -17,12 +17,14 @@ def create_app() -> Flask: from app.routes.devices import bp as devices_bp from app.routes.test_op import bp as test_op_bp from app.routes.test_data import bp as test_data_bp + from app.routes.fixture import bp as fixture_bp from app.routes.users import bp as users_bp from app.routes.logs import bp as logs_bp app.register_blueprint(devices_bp) app.register_blueprint(test_op_bp) app.register_blueprint(test_data_bp) + app.register_blueprint(fixture_bp) app.register_blueprint(users_bp) app.register_blueprint(logs_bp) diff --git a/edc-web/app/models.py b/edc-web/app/models.py index 46de7a7..1459363 100644 --- a/edc-web/app/models.py +++ b/edc-web/app/models.py @@ -108,6 +108,19 @@ def get_serialnet_records(dnt_id: int, limit: int = 50) -> list[dict]: conn.close() +def get_serialnet_by_id(record_id: int) -> dict | None: + """根据 ID 获取 tb_serialnet 记录""" + conn = get_conn() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM tb_serialnet WHERE id=%s", (record_id,), + ) + return cur.fetchone() + finally: + conn.close() + + def clear_serialnet_records(dnt_id: int): """清除指定设备的所有透传记录""" conn = get_conn() @@ -375,3 +388,143 @@ def get_logs(page: int = 1, per_page: int = 30, return cur.fetchall(), total finally: conn.close() + + +# ─── tb_fixture_param ────────────────────────────────────────────── + +def get_fixture_param(dnt_id: int) -> dict | None: + """获取设备的工装测试参数""" + conn = get_conn() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,), + ) + return cur.fetchone() + finally: + conn.close() + + +def upsert_fixture_param(dnt_id: int, **kwargs): + """插入或更新工装测试参数""" + conn = get_conn() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT id FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,), + ) + existing = cur.fetchone() + fields = [ + "Addr", "DevType", "TestMode", "RestDis", "MinusDis", + "SensMin", "SensMax", "FreMin", "FreMax", "PeakMin", "PeakMax", + ] + if existing: + sets = ", ".join(f"`{f}`=%s" for f in fields) + values = [kwargs.get(f, 0) for f in fields] + [dnt_id] + cur.execute( + f"UPDATE tb_fixture_param SET {sets} WHERE dnt_id=%s", + values, + ) + else: + placeholders = ", ".join(["%s"] * len(fields)) + col_names = ", ".join(f"`{f}`" for f in fields) + values = [kwargs.get(f, 0) for f in fields] + cur.execute( + f"INSERT INTO tb_fixture_param (dnt_id, {col_names}) " + f"VALUES (%s, {placeholders})", + [dnt_id] + values, + ) + conn.commit() + finally: + conn.close() + + +# ─── tb_vechicle_base_test ───────────────────────────────────────── + +def get_vehicle_base_tests(search: str = "") -> list[dict]: + """获取车检器测试基准参数列表""" + conn = get_conn() + try: + with conn.cursor() as cur: + if search: + cur.execute( + "SELECT * FROM tb_vechicle_base_test " + "WHERE dev_name LIKE %s OR type_num LIKE %s " + "ORDER BY type_num ASC", + (f"%{search}%", f"%{search}%"), + ) + else: + cur.execute( + "SELECT * FROM tb_vechicle_base_test ORDER BY type_num ASC", + ) + return cur.fetchall() + finally: + conn.close() + + +def get_vehicle_base_test_by_id(test_id: int) -> dict | None: + """根据 ID 获取车检器测试基准""" + conn = get_conn() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM tb_vechicle_base_test WHERE id=%s", (test_id,), + ) + return cur.fetchone() + finally: + conn.close() + + +def create_vehicle_base_test(**kwargs) -> int: + """创建车检器测试基准,返回新记录 ID""" + conn = get_conn() + try: + with conn.cursor() as cur: + fields = [ + "dev_name", "type_num", "SensMin", "SensMax", + "FreMin", "FreMax", "PeakMin", "PeakMax", "remark", + ] + col_names = ", ".join(f"`{f}`" for f in fields) + placeholders = ", ".join(["%s"] * len(fields)) + values = [kwargs.get(f, "" if f in ("dev_name", "remark") else 0) for f in fields] + cur.execute( + f"INSERT INTO tb_vechicle_base_test ({col_names}) VALUES ({placeholders})", + values, + ) + conn.commit() + return cur.lastrowid + finally: + conn.close() + + +def update_vehicle_base_test(test_id: int, **kwargs): + """更新车检器测试基准""" + conn = get_conn() + try: + with conn.cursor() as cur: + fields = [ + "dev_name", "type_num", "SensMin", "SensMax", + "FreMin", "FreMax", "PeakMin", "PeakMax", "remark", + ] + sets = ", ".join(f"`{f}`=%s" for f in fields) + values = [kwargs.get(f, "" if f in ("dev_name", "remark") else 0) for f in fields] + [test_id] + cur.execute( + f"UPDATE tb_vechicle_base_test SET {sets} WHERE id=%s", + values, + ) + conn.commit() + finally: + conn.close() + + +def delete_vehicle_base_test(test_id: int): + """删除车检器测试基准""" + conn = get_conn() + try: + with conn.cursor() as cur: + cur.execute( + "DELETE FROM tb_vechicle_base_test WHERE id=%s", (test_id,), + ) + conn.commit() + finally: + conn.close() diff --git a/edc-web/app/routes/fixture.py b/edc-web/app/routes/fixture.py new file mode 100644 index 0000000..2bb788f --- /dev/null +++ b/edc-web/app/routes/fixture.py @@ -0,0 +1,258 @@ +"""工装配置 API""" + +from flask import Blueprint, jsonify, render_template, request +from flask_login import login_required, current_user +from app.models import ( + get_device_by_id, + insert_serialnet, + get_fixture_param, + upsert_fixture_param, + get_vehicle_base_tests, + get_vehicle_base_test_by_id, + create_vehicle_base_test, + update_vehicle_base_test, + delete_vehicle_base_test, + get_serialnet_by_id, + insert_log, +) + +bp = Blueprint("fixture", __name__) + +# DG430 工装配置指令 (addr=0x01) +FIXTURE_COMMANDS = { + "4A": "7F81014ACACC", # 获取设备版本号 + "4C": "7F81014CCCCE", # 查询设备测试参数 + "4D": "7F81014DCDCF", # 出厂初始化 + "4E": "7F81014ECED0", # 设备复位 +} + +CMD_NAMES = { + "4A": "获取设备版本号", + "4B": "配置设备测试参数", + "4C": "查询设备测试参数", + "4D": "出厂初始化", + "4E": "设备复位", +} + + +def _xor_sum(data: bytes) -> tuple[int, int]: + """计算异或和校验 (从 ADDR 到 DATA 末)""" + xor = 0 + s = 0 + for b in data: + xor ^= b + s += b + return xor & 0xFF, s & 0xFF + + +def _le16(val: int) -> bytes: + """int → 小端 2 字节""" + return bytes([val & 0xFF, (val >> 8) & 0xFF]) + + +def _be16(val: int) -> bytes: + """int → 大端 2 字节""" + return bytes([(val >> 8) & 0xFF, val & 0xFF]) + + +def build_4b_packet(addr: int, dev_type: int, test_mode: int, + reset_dis: int, minus_dis: int, + sens_min: int, sens_max: int, + fre_min: int, fre_max: int, + peak_min: int, peak_max: int) -> str: + """构造 0x4B 配置指令 hex 字符串 + + 格式: 7F | 81 | 12 | 4B | Addr(1) | DevType(1) | TestMode(1) | + ResetDis(1) | MinusDis(1) | + SensMin(2 LE) | SensMax(2 LE) | FreMin(2 LE) | FreMax(2 LE) | + PeakMin(2 LE) | PeakMax(2 LE) | XOR | SUM + """ + payload = bytes([ + 0x4B, # CMD + addr & 0xFF, + dev_type & 0xFF, + test_mode & 0xFF, + reset_dis & 0xFF, + minus_dis & 0xFF, + ]) + payload += (_le16(sens_min) + _le16(sens_max) + + _le16(fre_min) + _le16(fre_max) + + _le16(peak_min) + _le16(peak_max)) + + pkt = bytes([0x7F, 0x81, len(payload)]) + payload + xor, total = _xor_sum(pkt[1:]) + pkt += bytes([xor, total]) + return pkt.hex().upper() + + +# ─── 页面 ─────────────────────────────────────────────────────────── + +@bp.route("/fixture/") +@login_required +def fixture_page(dnt_id): + """工装配置页面""" + device = get_device_by_id(dnt_id) + if not device: + return "设备不存在", 404 + return render_template("fixture.html", device=device) + + +@bp.route("/vehicle-base-test") +@login_required +def vehicle_base_test_page(): + """车检器测试基准参数管理页面""" + return render_template("vehicle_base_test.html") + + +# ─── 工装配置指令 API ────────────────────────────────────────────── + +@bp.route("/api/fixture/command", methods=["POST"]) +@login_required +def api_fixture_command(): + """发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)""" + data = request.get_json() + dnt_id = data.get("dnt_id") + cmd = data.get("cmd", "").upper() + + device = get_device_by_id(dnt_id) + target = f"{device['serial']}" if device else f"dnt_id={dnt_id}" + + if cmd == "4B": + # 动态构造 0x4B 指令 + params = data.get("params", {}) + send_pkg = build_4b_packet( + addr=params.get("addr", 1), + dev_type=params.get("dev_type", 0), + test_mode=params.get("test_mode", 0), + reset_dis=params.get("reset_dis", 0), + minus_dis=params.get("minus_dis", 0), + sens_min=params.get("sens_min", 0), + sens_max=params.get("sens_max", 0), + fre_min=params.get("fre_min", 0), + fre_max=params.get("fre_max", 0), + peak_min=params.get("peak_min", 0), + peak_max=params.get("peak_max", 0), + ) + elif cmd in FIXTURE_COMMANDS: + send_pkg = FIXTURE_COMMANDS[cmd] + else: + return jsonify({"ok": False, "error": f"未知指令: {cmd}"}), 400 + + cmd_name = CMD_NAMES.get(cmd, cmd) + try: + record_id = insert_serialnet(dnt_id, send_pkg) + insert_log( + current_user.id, current_user.username, "command", + target=target, + detail=f"工装配置: {cmd_name}(0x{cmd}) → {send_pkg}", + result="ok", + ip=request.remote_addr or "", + ) + return jsonify({"ok": True, "record_id": record_id, "send_pkg": send_pkg}) + except Exception as e: + insert_log( + current_user.id, current_user.username, "command", + target=target, + detail=f"工装配置 {cmd_name}(0x{cmd}) 失败: {e}", + result="error", + ip=request.remote_addr or "", + ) + return jsonify({"ok": False, "error": str(e)}), 500 + + +@bp.route("/api/fixture/serialnet/") +@login_required +def api_get_serialnet(record_id): + """查询 tb_serialnet 记录状态和返回数据""" + rec = get_serialnet_by_id(record_id) + if not rec: + return jsonify({"error": "记录不存在"}), 404 + return jsonify({ + "id": rec["id"], + "state": rec["state"], + "send_pkg": rec.get("send_pkg", ""), + "rcv_pkg": rec.get("rcv_pkg", ""), + "create_time": str(rec.get("create_time", "")), + "update_time": str(rec.get("update_time", "")), + }) + + +# ─── 工装参数 CRUD API ───────────────────────────────────────────── + +@bp.route("/api/fixture/param/") +@login_required +def api_get_fixture_param(dnt_id): + """获取工装测试参数""" + param = get_fixture_param(dnt_id) + return jsonify(param or {}) + + +@bp.route("/api/fixture/param/", methods=["POST"]) +@login_required +def api_save_fixture_param(dnt_id): + """保存工装测试参数(仅数据库,不下发设备)""" + data = request.get_json() + if not data: + return jsonify({"ok": False, "error": "数据为空"}), 400 + upsert_fixture_param(dnt_id, **data) + return jsonify({"ok": True}) + + +# ─── 车检器测试基准参数 CRUD API ────────────────────────────────── + +@bp.route("/api/vehicle-base-test") +@login_required +def api_list_vehicle_base_tests(): + """列出车检器测试基准参数""" + search = request.args.get("search", "") + tests = get_vehicle_base_tests(search) + return jsonify(tests) + + +@bp.route("/api/vehicle-base-test/") +@login_required +def api_get_vehicle_base_test(test_id): + """获取单个车检器测试基准""" + test = get_vehicle_base_test_by_id(test_id) + if not test: + return jsonify({"error": "不存在"}), 404 + return jsonify(test) + + +@bp.route("/api/vehicle-base-test", methods=["POST"]) +@login_required +def api_create_vehicle_base_test(): + """创建车检器测试基准""" + data = request.get_json() + if not data: + return jsonify({"ok": False, "error": "数据为空"}), 400 + try: + test_id = create_vehicle_base_test(**data) + return jsonify({"ok": True, "id": test_id}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +@bp.route("/api/vehicle-base-test/", methods=["PUT"]) +@login_required +def api_update_vehicle_base_test(test_id): + """更新车检器测试基准""" + data = request.get_json() + if not data: + return jsonify({"ok": False, "error": "数据为空"}), 400 + try: + update_vehicle_base_test(test_id, **data) + return jsonify({"ok": True}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +@bp.route("/api/vehicle-base-test/", methods=["DELETE"]) +@login_required +def api_delete_vehicle_base_test(test_id): + """删除车检器测试基准""" + try: + delete_vehicle_base_test(test_id) + return jsonify({"ok": True}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 diff --git a/edc-web/app/static/css/style.css b/edc-web/app/static/css/style.css index a9081ff..2f234d3 100644 --- a/edc-web/app/static/css/style.css +++ b/edc-web/app/static/css/style.css @@ -32,6 +32,8 @@ tr:hover { background: #f8f9fa; } /* === Buttons === */ .btn-test { padding: 4px 14px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; } .btn-test:hover { background: #2980b9; } +.btn-config { padding: 4px 14px; background: #9b59b6; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 4px; } +.btn-config:hover { background: #8e44ad; } /* === Test Page Layout === */ .test-header { margin-bottom: 20px; } @@ -103,3 +105,67 @@ tr:hover { background: #f8f9fa; } .search-bar { justify-content: center; } .btn-export { margin-left: 0; } } + +/* === Fixture Page === */ +.fixture-layout { display: flex; gap: 24px; } +.fixture-left { flex: 1; min-width: 420px; } +.fixture-right { flex: 1; min-width: 380px; } + +.fixture-card { background: #fff; padding: 16px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); margin-bottom: 16px; } +.fixture-card h3 { margin: 0 0 12px; font-size: 15px; color: #555; } + +.fixture-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; } +.fixture-form .form-group { display: flex; flex-direction: column; } +.fixture-form .form-group label { font-size: 12px; color: #777; margin-bottom: 3px; } +.fixture-form .form-group input, +.fixture-form .form-group select { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; } + +.fixture-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } +.btn-fixture { padding: 8px 14px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; font-size: 12px; transition: .15s; background: #ecf0f1; } +.btn-fixture:hover { background: #3498db; color: #fff; border-color: #3498db; } +.btn-fixture.danger { color: #e74c3c; border-color: #e74c3c; } +.btn-fixture.danger:hover { background: #e74c3c; color: #fff; } +.btn-fixture.primary { background: #3498db; color: #fff; border-color: #3498db; } +.btn-fixture.primary:hover { background: #2980b9; } + +.version-info { margin-top: 8px; padding: 8px 12px; background: #f0f8ff; border-radius: 4px; font-size: 13px; color: #2980b9; } + +.msg-toast { position: fixed; top: 20px; right: 20px; background: #27ae60; color: #fff; padding: 10px 20px; border-radius: 6px; font-size: 14px; z-index: 1000; opacity: 0; transition: opacity .3s; } +.msg-toast.show { opacity: 1; } +.msg-toast.error { background: #e74c3c; } + +/* === Vehicle base test table in fixture page === */ +.ref-table-wrapper { max-height: 300px; overflow-y: auto; margin-top: 8px; } +.ref-table-wrapper table { font-size: 12px; } +.ref-table-wrapper th, .ref-table-wrapper td { padding: 6px 8px; } +.ref-table-wrapper tr { cursor: pointer; } +.ref-table-wrapper tr:hover { background: #ebf5fb; } +.ref-table-wrapper tr.selected { background: #d4e6f1; } + +/* === Vehicle Base Test management === */ +.vbt-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } +.btn-add { padding: 8px 18px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; } +.btn-add:hover { background: #219a52; } +.btn-edit { padding: 3px 10px; background: #f39c12; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; } +.btn-edit:hover { background: #e67e22; } +.btn-del { padding: 3px 10px; background: #e74c3c; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; margin-left: 4px; } +.btn-del:hover { background: #c0392b; } + +/* === Modal === */ +.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center; z-index: 999; } +.modal-box { background: #fff; border-radius: 10px; padding: 24px; width: 480px; max-width: 95vw; box-shadow: 0 8px 32px rgba(0,0,0,.15); } +.modal-box h3 { margin: 0 0 16px; font-size: 16px; } +.modal-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; } +.modal-form .form-group { display: flex; flex-direction: column; } +.modal-form .form-group.full { grid-column: 1 / -1; } +.modal-form label { font-size: 12px; color: #777; margin-bottom: 3px; } +.modal-form input, .modal-form select, .modal-form textarea { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; } +.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; } +.btn-save { padding: 8px 20px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; } +.btn-save:hover { background: #2980b9; } +.btn-cancel { padding: 8px 20px; background: #ecf0f1; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } + +/* === Communication Log === */ +#comm-log { word-break: break-all; } +#comm-log::-webkit-scrollbar { width: 6px; } +#comm-log::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; } diff --git a/edc-web/app/static/js/devices.js b/edc-web/app/static/js/devices.js index ac26742..84ad17e 100644 --- a/edc-web/app/static/js/devices.js +++ b/edc-web/app/static/js/devices.js @@ -22,6 +22,7 @@ function renderTable(devices) { ${d.last_login || '-'} + `).join(""); diff --git a/edc-web/app/static/js/fixture.js b/edc-web/app/static/js/fixture.js new file mode 100644 index 0000000..196f277 --- /dev/null +++ b/edc-web/app/static/js/fixture.js @@ -0,0 +1,338 @@ +// 工装配置页 + +let baseTests = []; // 所有车检器基准参数 +let selectedBaseTest = null; +let pollTimer4C = null; // 0x4C 参数查询轮询 +let pollTimers = {}; // record_id → timer (指令响应轮询) + +// ─── 初始化 ───────────────────────────────── + +async function init() { + await loadBaseTests(); + 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} + ${t.FreMin}~${t.FreMax} + ${t.PeakMin}~${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 = t.FreMin; + document.getElementById("param-fre-max").value = t.FreMax; + document.getElementById("param-peak-min").value = t.PeakMin; + document.getElementById("param-peak-max").value = 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(); } +} + +// ─── 从 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; + document.getElementById("param-minus-dis").value = param.MinusDis || 0; + 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 = param.FreMin || 0; + document.getElementById("param-fre-max").value = param.FreMax || 0; + document.getElementById("param-peak-min").value = param.PeakMin || 0; + document.getElementById("param-peak-max").value = param.PeakMax || 0; + const matched = baseTests.find(t => t.type_num === param.DevType); + if (matched) { selectedBaseTest = matched; renderBaseTestTable(); } +} + +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(); + try { + const resp = await fetch(`/api/fixture/param/${DNT_ID}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + 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, + }), + }); + const result = await resp.json(); + if (result.ok) { + commLog('info', null, '参数已保存到数据库'); + toast("已保存到数据库"); + } else { + toast("保存失败: " + (result.error || ""), true); + } + } catch (e) { toast("保存失败: " + e.message, true); } +} + +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: parseInt(document.getElementById("param-reset-dis").value) || 0, + minus_dis: parseInt(document.getElementById("param-minus-dis").value) || 0, + sens_min: parseInt(document.getElementById("param-sens-min").value) || 0, + sens_max: parseInt(document.getElementById("param-sens-max").value) || 0, + fre_min: parseInt(document.getElementById("param-fre-min").value) || 0, + fre_max: parseInt(document.getElementById("param-fre-max").value) || 0, + peak_min: parseInt(document.getElementById("param-peak-min").value) || 0, + peak_max: parseInt(document.getElementById("param-peak-max").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"); + + // 同时保存到数据库 + await fetch(`/api/fixture/param/${DNT_ID}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + 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, + }), + }); + } 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(); diff --git a/edc-web/app/static/js/vehicle_base_test.js b/edc-web/app/static/js/vehicle_base_test.js new file mode 100644 index 0000000..66449d3 --- /dev/null +++ b/edc-web/app/static/js/vehicle_base_test.js @@ -0,0 +1,160 @@ +// 车检器测试基准参数管理 + +let editId = null; // null=新增, number=编辑 + +// ─── 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); +} + +// ─── 列表加载 ──────────────────────────────── + +async function loadList() { + const search = document.getElementById("search-input").value; + try { + const resp = await fetch(`/api/vehicle-base-test?search=${encodeURIComponent(search)}`); + const data = await resp.json(); + renderTable(data); + } catch (e) { + console.error("加载失败:", e); + } +} + +function renderTable(data) { + const tbody = document.querySelector("#vbt-table tbody"); + if (!data.length) { + tbody.innerHTML = '暂无数据,点右上角「新增」添加'; + return; + } + tbody.innerHTML = data.map(t => ` + + ${t.type_num} + ${esc(t.dev_name)} + ${t.SensMin} ~ ${t.SensMax} + ${t.FreMin} ~ ${t.FreMax} + ${t.PeakMin} ~ ${t.PeakMax} + ${esc(t.remark || '-')} + + + + + + `).join(""); +} + +// ─── 弹窗 ──────────────────────────────────── + +function openModal(id = null) { + editId = id; + document.getElementById("modal-title").textContent = id ? "编辑车检器测试基准" : "新增车检器测试基准"; + document.getElementById("edit-modal").style.display = "flex"; + + if (id) { + // 编辑:加载数据 + fetch(`/api/vehicle-base-test/${id}`) + .then(r => r.json()) + .then(data => { + document.getElementById("edit-type-num").value = data.type_num; + document.getElementById("edit-dev-name").value = data.dev_name; + document.getElementById("edit-sens-min").value = data.SensMin; + document.getElementById("edit-sens-max").value = data.SensMax; + document.getElementById("edit-fre-min").value = data.FreMin; + document.getElementById("edit-fre-max").value = data.FreMax; + document.getElementById("edit-peak-min").value = data.PeakMin; + document.getElementById("edit-peak-max").value = data.PeakMax; + document.getElementById("edit-remark").value = data.remark || ""; + }); + } else { + // 新增:清空 + document.getElementById("edit-type-num").value = ""; + document.getElementById("edit-dev-name").value = ""; + document.getElementById("edit-sens-min").value = "0"; + document.getElementById("edit-sens-max").value = "0"; + document.getElementById("edit-fre-min").value = "0"; + document.getElementById("edit-fre-max").value = "0"; + document.getElementById("edit-peak-min").value = "0"; + document.getElementById("edit-peak-max").value = "0"; + document.getElementById("edit-remark").value = ""; + } +} + +function closeModal() { + document.getElementById("edit-modal").style.display = "none"; + editId = null; +} + +// ─── 保存 ──────────────────────────────────── + +async function saveRecord() { + const data = { + type_num: parseInt(document.getElementById("edit-type-num").value) || 0, + dev_name: document.getElementById("edit-dev-name").value.trim(), + SensMin: parseInt(document.getElementById("edit-sens-min").value) || 0, + SensMax: parseInt(document.getElementById("edit-sens-max").value) || 0, + FreMin: parseInt(document.getElementById("edit-fre-min").value) || 0, + FreMax: parseInt(document.getElementById("edit-fre-max").value) || 0, + PeakMin: parseInt(document.getElementById("edit-peak-min").value) || 0, + PeakMax: parseInt(document.getElementById("edit-peak-max").value) || 0, + remark: document.getElementById("edit-remark").value.trim(), + }; + + if (!data.dev_name) { + toast("请输入型号/名称", true); + return; + } + + try { + let resp; + if (editId) { + resp = await fetch(`/api/vehicle-base-test/${editId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + } else { + resp = await fetch("/api/vehicle-base-test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + } + const result = await resp.json(); + if (result.ok || resp.ok) { + toast(editId ? "更新成功" : "新增成功"); + closeModal(); + loadList(); + } else { + toast("保存失败: " + (result.error || "未知错误"), true); + } + } catch (e) { + toast("保存失败: " + e.message, true); + } +} + +// ─── 删除 ──────────────────────────────────── + +async function deleteRecord(id, name) { + if (!confirm(`确定要删除「${name}」吗?`)) return; + + try { + const resp = await fetch(`/api/vehicle-base-test/${id}`, { method: "DELETE" }); + const result = await resp.json(); + if (result.ok) { + toast("删除成功"); + loadList(); + } else { + toast("删除失败: " + (result.error || "未知错误"), true); + } + } catch (e) { + toast("删除失败: " + e.message, true); + } +} + +function esc(s) { return (s || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } + +loadList(); diff --git a/edc-web/app/templates/fixture.html b/edc-web/app/templates/fixture.html new file mode 100644 index 0000000..a930c13 --- /dev/null +++ b/edc-web/app/templates/fixture.html @@ -0,0 +1,128 @@ +{% extends "base.html" %} +{% block title %}工装配置 - {{ device.serial }} - EDC 工装管理系统{% endblock %} + +{% block content %} +
+ ← 返回设备列表 +

工装配置 —— {{ device.serial }} ({{ device.name or '未命名' }})

+
+ +
+ +
+
+
+

工装测试参数

+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + + +
+ +
+ + +
+

通信日志

+
+
等待操作…
+
+
+
+ + +
+
+
+

车检器测试基准参数

+
+ + +
+
+
+ + + + + + + +
编码名称灵敏度频率(Hz)峰峰值
+
+
+
+
+ +
+{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/edc-web/app/templates/vehicle_base_test.html b/edc-web/app/templates/vehicle_base_test.html new file mode 100644 index 0000000..bd41b44 --- /dev/null +++ b/edc-web/app/templates/vehicle_base_test.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} +{% block title %}车检器测试基准参数管理 - EDC 工装管理系统{% endblock %} + +{% block content %} +
+ ← 返回设备列表 +

车检器测试基准参数管理

+
+ +
+
+
+ +
+ +
+ + + + + + + + + + + + + + +
类型编码型号/名称灵敏度范围频率范围 (Hz)峰峰值范围备注操作
+
+ + + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/edc_server b/edc_server index 43fd3e7..e7c20c6 160000 --- a/edc_server +++ b/edc_server @@ -1 +1 @@ -Subproject commit 43fd3e7be9f5d66628f5d9bff510e9ef3eb532b8 +Subproject commit e7c20c69d26ca6dd32e44d5013f4627b1c83cb32