添加NFC绑定红外设备方式,融合车端设备和扩展设备统一控制

This commit is contained in:
minghanchen
2026-09-01 11:04:59 +08:00
parent 6d75944c0f
commit ca26259e63
14 changed files with 3694 additions and 316 deletions
+94 -21
View File
@@ -44,7 +44,9 @@
if (validatedCommandContext.requestId && requestId
&& validatedCommandContext.requestId !== requestId) return false;
if (validatedCommandContext.utterance && utterance
&& validatedCommandContext.utterance !== utterance) return false;
&& validatedCommandContext.utterance !== utterance
&& !validatedCommandContext.utterance.includes(utterance)
&& !utterance.includes(validatedCommandContext.utterance)) return false;
return true;
}
@@ -105,6 +107,22 @@
return `0x300 · ${hexByte(opcode)} ${hexByte(control)} <SEQ_LO> <SEQ_HI> ${littleEndian16(paramA)} ${littleEndian16(paramB)}`;
};
if (capability === "infrared.emit") {
const slot = boundedInteger(parameters.slot, 1, 248);
const action = String(parameters.action || "红外动作").trim();
const deviceName = String(operation.target?.name || operation.target?.device || "红外外设").trim();
if (slot !== null) {
return {
channel: "IR",
category: `${deviceName} · ${action}`,
command: `infrared emit ${slot}`,
frame: JSON.stringify({ action: "emit", data: { no: slot }, type: "infrared" }),
frameLabel: "GSCU1B TCP",
...(parseSource ? { parseSource } : {}),
};
}
}
const isPassengerSeat = new Set(["passenger", "front-right"]).has(target);
if (isPassengerSeat) {
const passengerAxes = {
@@ -324,23 +342,74 @@
function parsedCanCommands(result = {}) {
const configurations = result.function_configs || {};
const plans = result.command_plans || {};
const planKeys = new Set(Object.keys(plans));
const hasCompiledPlan = planKeys.size > 0 || Boolean(result.command_plan);
if (!hasCompiledPlan) return [];
const configurationEntries = Object.entries(configurations);
if (!configurationEntries.length && result.function_config) {
configurationEntries.push(["default", result.function_config]);
}
const executionResults = [
...(result.execution_report?.spec?.results || []),
...(result.operation_results || []),
...Object.values(result.phase_execution_reports || {}).flatMap(
(report) => report?.spec?.results || [],
),
];
const executionByRef = new Map();
executionResults.forEach((execution) => {
const ref = String(execution?.ref || "");
if (!ref) return;
const previous = executionByRef.get(ref);
if (!previous || (!previous.telemetry && execution?.telemetry)) {
executionByRef.set(ref, execution);
}
});
const submittedStates = new Set(["submitted", "queued", "published", "sent", "delivered"]);
const deliveryForOperation = (operation) => {
const capability = String(operation?.capability || "");
const operationId = String(operation?.id || "");
const execution = executionByRef.get(operationId) || executionResults.find((candidate) => {
const ref = String(candidate?.ref || "");
return ref && operationId && (
operationId.startsWith(`${ref}-`) || ref.startsWith(`${operationId}-`)
);
});
const telemetry = execution?.telemetry || {};
const mqttMessages = Array.isArray(telemetry.mqtt_messages) ? telemetry.mqtt_messages : [];
const deliveryStates = [
telemetry.delivery_status,
...mqttMessages.map((message) => message?.delivery_status),
].map((value) => String(value || "").toLowerCase());
const submitted = result.execution_mode === "hardware"
&& telemetry.simulated !== true
&& (
deliveryStates.some((value) => submittedStates.has(value))
|| (Array.isArray(telemetry.sent_frames) && telemetry.sent_frames.length > 0)
|| telemetry.gateway_acknowledged === true
|| telemetry.feedback_observed === true
|| telemetry.physical_effect_confirmed === true
);
const transportLabel = capability === "infrared.emit"
? "红外 TCP"
: result.control_transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
return {
deliveryState: submitted ? "submitted" : "not_submitted",
deliveryLabel: submitted ? "下发" : "未下发",
deliveryTransport: transportLabel,
};
};
const commands = [];
Object.entries(configurations).forEach(([configKey, configuration]) => {
if (planKeys.size > 0 && !planKeys.has(configKey)) return;
configurationEntries.forEach(([_configKey, configuration]) => {
(configuration?.spec?.operations || []).forEach((operation) => {
const item = parsedCanCommandForOperation(operation);
if (item) commands.push(item);
if (item) commands.push({ ...item, ...deliveryForOperation(operation) });
});
});
const unique = new Map();
commands.forEach((item) => {
const key = `${item.channel}:${item.command}`;
const previous = unique.get(key);
if (!previous || (previous.parseSource !== "local_rule" && item.parseSource === "local_rule")) {
if (!previous
|| (previous.deliveryState !== "submitted" && item.deliveryState === "submitted")
|| (previous.parseSource !== "local_rule" && item.parseSource === "local_rule")) {
unique.set(key, item);
}
});
@@ -357,17 +426,18 @@
panel.dataset.state = commands.length ? "ready" : message ? "pending" : "empty";
if (commands.length) {
list.innerHTML = `<ol>${commands.map((item) => `
<li data-can-channel="${escapeHtml(item.channel)}">
<li data-can-channel="${escapeHtml(item.channel)}" data-delivery-state="${escapeHtml(item.deliveryState)}">
<span>
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${item.parseSource === "direct_control" ? "设备控件" : item.parseSource === "local_rule" ? "本地规则" : "云端模型"}</b>` : ""}
${escapeHtml(item.category)}
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml(`${item.deliveryTransport} · ${item.deliveryLabel}`)}">${escapeHtml(item.deliveryLabel)}</b>
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
</span>
<code>${escapeHtml(item.command)}</code>
${item.frame ? `<small class="v2-can-command-frame">固件原生 CAN0 · ${escapeHtml(item.frame)}</small>` : ""}
${item.frame ? `<small class="v2-can-command-frame">${escapeHtml(item.frameLabel || "固件原生 CAN0")} · ${escapeHtml(item.frame)}</small>` : ""}
</li>`).join("")}</ol>`;
} else {
list.innerHTML = `<p>${escapeHtml(message || "本次请求未生成已验证的 CAN 控制指令")}</p>`;
list.innerHTML = `<p>${escapeHtml(message || "本次请求未解析出控制指令")}</p>`;
}
requestAnimationFrame(syncSidebarLogBaseline);
}
@@ -380,15 +450,18 @@
const items = commands.map((command) => ({
channel: command.startsWith("seat passenger ") ? "CAN0" : "CAN1",
command,
deliveryState: "not_submitted",
deliveryLabel: "未下发",
}));
count.textContent = String(items.length);
panel.dataset.state = "ready";
list.innerHTML = `<ol>${items.map((item) => `
<li data-can-channel="${item.channel}">
<li data-can-channel="${item.channel}" data-delivery-state="${item.deliveryState}">
<span>
<b class="v2-can-command-channel">${item.channel}</b>
<b class="v2-can-command-source" data-parse-source="direct_control">设备控件</b>
${escapeHtml(deviceName)}
<b class="v2-can-command-delivery" data-delivery-state="${item.deliveryState}" title="尚未进入 CAN 传输链路">${item.deliveryLabel}</b>
<i class="v2-can-command-category">${escapeHtml(deviceName)}</i>
</span>
<code>${escapeHtml(item.command)}</code>
</li>`).join("")}</ol>`;
@@ -1205,7 +1278,7 @@
: trace.length
? trace.slice(-4).map((item) => item.label || item.stage || item.event || "执行步骤")
: domains.length
? ["接收编排指令", `编排 ${domains.length} 个设备域`, "下发执行计划", "同步执行结果"]
? ["接收编排指令", `编排 ${domains.length} 个设备域`, "生成执行计划", "同步执行结果"]
: ["等待车机或网页端下发编排任务"];
timeline.innerHTML = stages.map((stage, index) => `
<span class="${task?.status === "running" && index === stages.length - 1 ? "active" : "done"}">
@@ -1258,7 +1331,7 @@
// `running` snapshot for the same task must not erase those commands.
if (!validatedCommandsBelongToTask(task)) {
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
renderParsedCanCommands(null, { message: "正在解析自然语言并生成控制指令…" });
}
} else if (result) {
const commands = parsedCanCommands(result);
@@ -1451,7 +1524,7 @@
insertVisibleControl(simulationFaultField, "v2-sync-simulation-fault-field");
const canCommandPanel = make("details", "v2-can-command-panel", `
<summary>
<span><i data-lucide="binary"></i><b>已解析 CAN 控制指令</b></span>
<span><i data-lucide="binary"></i><b>已解析控制指令</b></span>
<strong id="v2CanCommandCount">0</strong>
<i data-lucide="chevron-down"></i>
</summary>
@@ -1550,7 +1623,7 @@
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
$("#requestForm")?.addEventListener("submit", () => {
resetValidatedCommandContext();
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
renderParsedCanCommands(null, { message: "正在解析自然语言并生成控制指令…" });
});
window.addEventListener("cognitive-agent:validated-can-commands", (event) => {
const result = event.detail || {};
@@ -1558,7 +1631,7 @@
if (validatedCommandsVisible) rememberValidatedCommandContext(result);
else resetValidatedCommandContext();
renderParsedCanCommands(result, {
message: "已完成白名单校验,正在下发控制指令…",
message: "控制指令已解析,正在等待传输结果…",
});
});
window.addEventListener("cognitive-agent:result", (event) => {
@@ -1591,7 +1664,7 @@
});
window.addEventListener("cognitive-agent:request-complete", (event) => {
if (event.detail?.error && !validatedCommandsVisible) {
renderParsedCanCommands(null, { message: "本次请求未生成 CAN 控制指令" });
renderParsedCanCommands(null, { message: "本次请求未生成控制指令" });
}
if (event.detail?.error) {
const error = event.detail.error;