fix(ui): 电机前端回滚到 P1 拆分前,恢复 STATUS 离线层契约
根因:拆分后 renderStatusPanel 操作 #statusOffline/#statusPanel, HTML 实际 id 是 offlineOverlay/statusLive,遮罩永不摘掉。 CSS/HTML 结构本就未重写;本提交恢复完整 main.js 与 dual-link 传输层。
This commit is contained in:
@@ -22,29 +22,9 @@
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
function bytesToHexStr(bytes) {
|
||||
let s = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
s += bytes[i].toString(16).padStart(2, "0").toUpperCase();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function hexToBytes(hex) {
|
||||
const clean = String(hex || "").replace(/[^0-9a-fA-F]/g, "");
|
||||
const out = new Uint8Array(Math.floor(clean.length / 2));
|
||||
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.substr(i * 2, 2), 16);
|
||||
return out;
|
||||
}
|
||||
function u16LeAt(bytes, i) { return bytes[i] | (bytes[i + 1] << 8); }
|
||||
function canId(node, suffix) { return ((node & 0xF) << 16) | (suffix & 0xFFFF); }
|
||||
const ID_GET_NODE_ID = (n) => canId(n, 0x0401);
|
||||
const ID_SET_NODE_ID = (n) => canId(n, 0x0400);
|
||||
|
||||
/** @description 创建 WiFi/HTTP 传输实例。 */
|
||||
export function createWifiTransport(ctx) {
|
||||
const { getBaseUrl, recordSlow, isTransient } = ctx;
|
||||
const sendFrame = ctx.sendFrame;
|
||||
const getCh = typeof ctx.getCh === "function" ? ctx.getCh : () => 0;
|
||||
|
||||
/** 统一 GET JSON。失败抛 Error,附 status / transient(与原 fetchDeviceJson 一致)。 */
|
||||
async function getJson(path, timeoutMs) {
|
||||
@@ -249,99 +229,6 @@ export function createWifiTransport(ctx) {
|
||||
return data.nodes.find((n) => n.node === node) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* R5:版本等待下沉传输层。
|
||||
* WiFi 走 GD32 缓存 /motor/version,按 seq 增长或 at>=sentAt 判新应答。
|
||||
* 超时返回 null(与旧 waitMotorVersionOnTick 语义一致)。
|
||||
*/
|
||||
async waitVersion(node, opts) {
|
||||
const prevSeq = opts && opts.prevSeq;
|
||||
const sentAt = (opts && opts.sentAt) || 0;
|
||||
const timeoutMs = (opts && opts.timeoutMs) || 5000;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const sampleOnce = async () => {
|
||||
try {
|
||||
const v = await getJson("/motor/version", 2000);
|
||||
if (!v || !v.have_data || !Array.isArray(v.nodes)) return null;
|
||||
const hit = v.nodes.find((n) => n.node === node) || null;
|
||||
if (hit && hit.version &&
|
||||
(prevSeq == null || hit.seq > prevSeq || (hit.at && hit.at >= sentAt))) {
|
||||
return hit;
|
||||
}
|
||||
} catch (_) { /* 瞬时 busy 忽略 */ }
|
||||
return null;
|
||||
};
|
||||
let v = await sampleOnce();
|
||||
if (v) return v;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
v = await sampleOnce();
|
||||
if (v) return v;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* R5:节点参数 GET(0x000N0401 扩展格式)。
|
||||
* 发送走注入的 sendOneFrameWhenFree(保持 main 流控);回执走 GD32 per-ID 快照。
|
||||
*/
|
||||
async queryNodeIdParam(node, selector) {
|
||||
if (typeof sendFrame !== "function") return { ok: false, desc: "sendFrame 未注入" };
|
||||
const bytes = new Uint8Array([selector & 0xFF, (selector >> 8) & 0xFF]);
|
||||
const id = ID_GET_NODE_ID(node);
|
||||
const res = await sendFrame(id, 2, bytesToHexStr(bytes));
|
||||
if (!res.ok) {
|
||||
return { ok: false, desc: res.desc || "发送失败", networkError: res.networkError, busy: res.busy };
|
||||
}
|
||||
let rb = null;
|
||||
const r = res.results && res.results[0];
|
||||
if (r && r.recv && r.recv_len >= 2) {
|
||||
const b = hexToBytes(r.recv);
|
||||
if (b.length === 2 || b.length === 4) rb = b;
|
||||
}
|
||||
if (!rb) {
|
||||
await new Promise((r2) => setTimeout(r2, 80));
|
||||
try {
|
||||
const fb = await getJson("/can/feedback", 2500);
|
||||
const ch = (fb && fb.channels) ? fb.channels[getCh()] : null;
|
||||
if (ch && ch.valid && Array.isArray(ch.entries)) {
|
||||
const hit = ch.entries.find((e) => e && Number(e.id) === id);
|
||||
if (hit) rb = hexToBytes(hit.data);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return parseParamReply(rb, selector);
|
||||
},
|
||||
|
||||
/** R5:节点参数 SET(0x000N0400 扩展格式 DLC4)。 */
|
||||
async setNodeIdParam(node, selector, value) {
|
||||
if (typeof sendFrame !== "function") return { ok: false, desc: "sendFrame 未注入" };
|
||||
const bytes = new Uint8Array([
|
||||
selector & 0xFF, (selector >> 8) & 0xFF,
|
||||
value & 0xFF, (value >> 8) & 0xFF,
|
||||
]);
|
||||
const id = ID_SET_NODE_ID(node);
|
||||
const res = await sendFrame(id, 4, bytesToHexStr(bytes));
|
||||
if (!res.ok) {
|
||||
return { ok: false, desc: res.desc || "发送失败", networkError: res.networkError, busy: res.busy };
|
||||
}
|
||||
let rb = null;
|
||||
const r = res.results && res.results[0];
|
||||
if (r && r.recv && r.recv_len >= 5) rb = hexToBytes(r.recv);
|
||||
if (!rb) {
|
||||
await new Promise((r2) => setTimeout(r2, 80));
|
||||
try {
|
||||
const fb = await getJson("/can/feedback", 2500);
|
||||
const ch = (fb && fb.channels) ? fb.channels[getCh()] : null;
|
||||
if (ch && ch.valid && Array.isArray(ch.entries)) {
|
||||
const hit = ch.entries.find((e) => e && Number(e.id) === id);
|
||||
if (hit) rb = hexToBytes(hit.data);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return parseSetReply(rb, selector);
|
||||
},
|
||||
|
||||
/** 把前端刷新周期同步给 GD32(0xC5 主动推送周期),失败静默。 */
|
||||
async setPeriod(ms) {
|
||||
const v = Math.max(20, Math.min(65535, Math.round(ms)));
|
||||
@@ -359,28 +246,3 @@ export function createWifiTransport(ctx) {
|
||||
async teardown() { /* no-op */ },
|
||||
};
|
||||
}
|
||||
|
||||
function parseParamReply(rb, selector) {
|
||||
if (!rb) return { ok: false, desc: "未收到应答(电机离线或未响应)" };
|
||||
if (rb.length >= 4) {
|
||||
const sel = u16LeAt(rb, 0), val = u16LeAt(rb, 2);
|
||||
if (sel !== selector) {
|
||||
return { ok: false, desc: `应答选择器不符(0x${sel.toString(16).padStart(4, "0")})` };
|
||||
}
|
||||
return { ok: true, val };
|
||||
}
|
||||
if (rb.length === 2) return { ok: true, oldFw: true, val: u16LeAt(rb, 0) };
|
||||
return { ok: false, desc: "应答格式异常" };
|
||||
}
|
||||
|
||||
function parseSetReply(rb, selector) {
|
||||
if (!rb) return { ok: false, desc: "未收到应答(电机离线或未响应)" };
|
||||
if (rb.length >= 5) {
|
||||
const st = rb[0], sel = u16LeAt(rb, 1);
|
||||
if (sel !== selector) {
|
||||
return { ok: false, desc: `应答选择器不符(0x${sel.toString(16).padStart(4, "0")})` };
|
||||
}
|
||||
return st === 1 ? { ok: true } : { ok: false, desc: "电机 NACK(Flash 写入失败或参数非法)" };
|
||||
}
|
||||
return { ok: false, desc: "应答格式异常(电机固件不支持刷新率参数?需 v0.4.1+)" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user