feat(ecat): PDO 快发 ESF1 桌面强制直通,堵死慢发回落

桌面 APP 下 ECAT motor 发送只走 pywebview ESF1→native TCP;
删除桥内 HTTP/JSON 回落。附延迟测量与 pack 自测脚本与规格文档。
This commit is contained in:
cbb
2026-09-21 02:11:49 +08:00
parent 383535bd1d
commit 2c6235d920
13 changed files with 1267 additions and 21 deletions
+46 -4
View File
@@ -44,7 +44,8 @@ import { staleLimitMs } from "./refresh-rate.js";
import { deviceLink } from "../common/device-link.js";
import {
desktopEcatPost, desktopBridgeLabel, getDesktopEcatApi,
} from "../common/desktop-bridge.js?v=20260923g";
desktopEcatSendRaw, hasDesktopFastSend,
} from "../common/desktop-bridge.js?v=20260923i";
/** 单个 PDO 周期回传的状态槽数。
* 固件 1.6.21 起段 2 密排整轮:一拍带齐总线电机(最多 9),空槽不占位。
@@ -242,6 +243,41 @@ export function createEcatTransport(ctx) {
return callHttp(tag, body, timeoutMs);
}
/**
* ECAT 发送统一入口。
* 桌面桥且 ecat_send_raw 可用:**只走 ESF1**,失败直接返回 ok:false,
* **禁止**回落 HTTP motor/send(2026-09-23 堵死慢发,防误走 JSON 路径)。
* 无桌面桥(浏览器标签):仍走 api("motor/send") HTTP。
* payload = {frames:[{ch,id,dlc,data}]}。
*/
let lastFastSendPath = "";
async function sendViaFastOrJson(payload) {
if (hasDesktopFastSend()) {
lastFastSendPath = "fast_tcp";
try {
const r = await desktopEcatSendRaw(payload.frames || [], 2000);
if (r && r.ok === true) {
if (r.path) lastFastSendPath = r.path;
return r;
}
const msg = (r && r.log && r.log.length)
? r.log.join(" / ")
: "二进制快发失败(已禁用 HTTP/JSON 回落)";
recordSlow("fast-send", 0, msg);
return { ok: false, queued: 0, backlog: 0, path: "fast_tcp", log: [msg] };
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
recordSlow("fast-send", 0, err.message);
return {
ok: false, queued: 0, backlog: 0, path: "fast_tcp",
log: [err.message || "二进制快发异常"],
};
}
}
lastFastSendPath = "json";
return api("motor/send", payload, 6000);
}
/** ok:false 是后端的常规业务错误信道(HTTP 仍是 200)。 */
function unwrap(json, what) {
if (!json || json.ok !== true) {
@@ -533,7 +569,10 @@ export function createEcatTransport(ctx) {
supportsPeriodSync: false,
sendHint(ch) {
return desktopBridgeLabel() + " · motor/send · CH" + ch +
const fast = lastFastSendPath === "fast_tcp"
? "二进制快发"
: desktopBridgeLabel();
return fast + " · motor/send · CH" + ch +
(periodMs ? " · PDO " + fmtPeriod(periodMs) : "");
},
@@ -706,6 +745,9 @@ export function createEcatTransport(ctx) {
* 在**同一个 seq 沿**由从站一次性发出 —— 这是多轴同步下发的前提,
* 逐帧调用会退化成一帧一个周期。
* 返回值补成 WiFi 的 results[] 形状(后端只回 queued/backlog)。
*
* PDO 快发:桌面桥有 ecat_send_raw 时**只走** ESF1 二进制直通,
* 失败 ok:false、不回落 JSON(堵死慢发)。无桥(浏览器)才走 motor/send。
*/
async sendFrames(frames, opts) {
if (!started) await ensureStarted();
@@ -714,7 +756,7 @@ export function createEcatTransport(ctx) {
ch: opts.ch, id: f.id, dlc: f.dlc, data: f.data,
})),
};
const r = await api("motor/send", payload, 6000);
const r = await sendViaFastOrJson(payload);
if (!r || r.ok !== true) {
const msg = (r && r.log && r.log.length) ? r.log.join(" / ") : "发送失败";
return frames.map(() => ({ status: "error", code: -2, gd32_err: 0, desc: msg }));
@@ -733,7 +775,7 @@ export function createEcatTransport(ctx) {
ch: f.ch,
}));
if (!started) await ensureStarted();
const r = await api("motor/send", { frames: built }, 6000);
const r = await sendViaFastOrJson({ frames: built });
if (!r || r.ok !== true) {
const msg = (r && r.log && r.log.length) ? r.log.join(" / ") : "发送失败";
return built.map(() => ({ status: "error", code: -2, gd32_err: 0, desc: msg }));