3 Commits

Author SHA1 Message Date
minghanchen 8d05815773 优化前端,优化前端指令,优化自定义控制 2026-08-24 09:40:16 +08:00
minghanchen fff7657fea merge: integrate wired and wireless CAN into N7Web 2026-08-19 10:21:27 +08:00
minghanchen 52c4dd08f9 添加无线模块 2026-08-19 09:59:36 +08:00
144 changed files with 10126 additions and 2295 deletions
+1
View File
@@ -4,6 +4,7 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
artifacts/runs/ artifacts/runs/
artifacts/mode-profiles/
artifacts/server/ artifacts/server/
artifacts/tmp/ artifacts/tmp/
artifacts/self-repair/ artifacts/self-repair/
+7
View File
@@ -113,6 +113,13 @@ config/gesture/default.yaml
同一局域网的其他电脑需要体验时,使用 `make dev-lan` 监听全部网络接口。服务启动日志会打印实际 LAN 地址,例如 `http://172.20.10.5:8000`。前端和 API 使用同源访问,不需要 CORS,也不会把 `OPENAI_API_KEY` 发送到浏览器。若没有启用 `.env.auth`,任何可达设备都能触发服务端 GPT 请求,因此未认证的 LAN 模式只应在可信网络内临时演示。 同一局域网的其他电脑需要体验时,使用 `make dev-lan` 监听全部网络接口。服务启动日志会打印实际 LAN 地址,例如 `http://172.20.10.5:8000`。前端和 API 使用同源访问,不需要 CORS,也不会把 `OPENAI_API_KEY` 发送到浏览器。若没有启用 `.env.auth`,任何可达设备都能触发服务端 GPT 请求,因此未认证的 LAN 模式只应在可信网络内临时演示。
Linux 上使用 USBCANFD-200U 被动同步 CH1 车辆状态时,使用
`zlgcan-linux` 后端。USB 权限、60 MHz/500k/2M 位时序、只读配置和状态接口
验证见 [Linux ZLG 车辆状态接入](docs/workflows/linux-zlg-vehicle-state.md)。
同一页面也可选择本机 NanoMQ 无线链路,其中 CAN0 对应副驾座椅、CAN1 对应
其余车辆状态和控制;MQTT 主题、原始 CAN 帧 JSON 合约和有线/无线共用安全
门禁见同一文档。
## 认证与公网体验 ## 认证与公网体验
公网演示必须启用 `.env.auth` 中的服务端认证。密码使用 PBKDF2-SHA256 哈希保存;认证成功后签发有有效期的 HMAC 会话 Cookie,Cookie 使用 `HttpOnly` 和 `SameSite=Strict`,经 HTTPS 代理时自动增加 `Secure`。登录失败按来源 IP 限流,GPT 请求按体验账号限制每分钟调用次数。未登录访问页面会跳转 `/login`,未登录 API 请求返回 `401`。 公网演示必须启用 `.env.auth` 中的服务端认证。密码使用 PBKDF2-SHA256 哈希保存;认证成功后签发有有效期的 HMAC 会话 Cookie,Cookie 使用 `HttpOnly` 和 `SameSite=Strict`,经 HTTPS 代理时自动增加 `Secure`。登录失败按来源 IP 限流,GPT 请求按体验账号限制每分钟调用次数。未登录访问页面会跳转 `/login`,未登录 API 请求返回 `401`。
+1168 -233
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+368 -325
View File
@@ -20,7 +20,6 @@
sidebarCollapsed: sessionStorage.getItem("cognitive-agent.sidebar-collapsed") === "true", sidebarCollapsed: sessionStorage.getItem("cognitive-agent.sidebar-collapsed") === "true",
}; };
const dropdownControls = new Map();
let reasoningPickerControl = null; let reasoningPickerControl = null;
function make(tag, className, html) { function make(tag, className, html) {
@@ -39,10 +38,261 @@
.replaceAll("'", "'"); .replaceAll("'", "'");
} }
function closeCustomDropdowns(except = null) { const normalizedCanTarget = (target = {}) => String(
dropdownControls.forEach((control) => { target.zone || target.instance || target.device || "vehicle",
if (control !== except) control.close(); ).replaceAll("_", "-");
const normalizedOnOff = (value) => {
if ([true, 1, "1", "on", "open", "enabled"].includes(value)) return "on";
if ([false, 0, "0", "off", "close", "disabled"].includes(value)) return "off";
return null;
};
const boundedInteger = (value, minimum, maximum) => {
const numeric = Number(value);
return Number.isInteger(numeric) && numeric >= minimum && numeric <= maximum
? numeric
: null;
};
function parsedCanCommandForOperation(operation = {}) {
const capability = String(operation.capability || "");
const parameters = operation.parameters || {};
const target = normalizedCanTarget(operation.target);
const onOff = normalizedOnOff(parameters.state ?? parameters.power);
const hyphenated = (value) => String(value || "").replaceAll("_", "-");
const command = (category, text, channel = "CAN1", frame = "") => ({
channel, category, command: text, ...(frame ? { frame } : {}),
}); });
const hexByte = (value) => Number(value).toString(16).padStart(2, "0").toUpperCase();
const otaCan0Frame = (opcode, selector, argument) => {
const numeric = Number(argument) >>> 0;
const argumentBytes = [0, 8, 16, 24].map((shift) => hexByte((numeric >>> shift) & 0xFF));
return `0x6A0 · C7 <SEQ> ${hexByte(opcode)} ${hexByte(selector)} ${argumentBytes.join(" ")}`;
};
const isPassengerSeat = new Set(["passenger", "front-right"]).has(target);
if (isPassengerSeat) {
const passengerAxes = {
"seat.set_longitudinal_position": ["副驾座椅前后调节", "longitudinal", 0],
"seat.set_cushion_tilt": ["副驾座椅高度调节", "height", 1],
"seat.set_backrest_recline": ["副驾座椅靠背调节", "backrest", 2],
"seat.set_legrest_height": ["副驾座椅腿托调节", "legrest", 3],
};
const axis = passengerAxes[capability];
const position = boundedInteger(parameters.position_pct, 0, 100);
if (axis && position !== null) {
const targetPermille = position * 10;
return command(
axis[0],
`seat passenger ${axis[1]} position ${position}`,
"CAN0",
otaCan0Frame(0x64, axis[2], targetPermille),
);
}
if (capability === "seat.start_dynamic_mode") {
const mode = boundedInteger(parameters.mode_id, 1, 2);
if (mode !== null) {
const dynamic = mode === 1
? { name: "rocking-horse", selector: 2 }
: { name: "cradle", selector: 1 };
return command(
"副驾座椅动态模式",
`seat passenger dynamic start ${dynamic.name}`,
"CAN0",
otaCan0Frame(0x66, dynamic.selector, 300),
);
}
}
if (capability === "seat.stop_dynamic_mode") {
return command(
"副驾座椅动态模式",
"seat passenger dynamic stop",
"CAN0",
otaCan0Frame(0x63, 0xFF, 0),
);
}
}
if (capability === "door.set_rear_lock") {
const lockState = String(parameters.state || "");
const action = new Set(["lock", "unlock"]).has(lockState)
? lockState
: onOff === "on" ? "lock" : onOff === "off" ? "unlock" : null;
if (action) return command("后排门锁", `door rear ${action}`);
}
if (capability === "door.set_vehicle_lock") {
const lockState = String(parameters.state || "");
if (new Set(["lock", "unlock"]).has(lockState)) {
return command("整车门锁", `door vehicle ${lockState}`);
}
}
if (capability === "window.set_motion") {
const scope = new Set(["all", "front", "rear", "front-left", "front-right", "rear-left", "rear-right"]);
const motion = String(parameters.motion || parameters.action || "");
if (scope.has(target) && new Set(["open", "close", "stop"]).has(motion)) {
return command("车窗动作", `window ${target} ${motion}`);
}
}
if (capability === "window.set_position") {
const position = boundedInteger(parameters.position_pct, 0, 100);
if (new Set(["front-left", "front-right", "rear-left", "rear-right"]).has(target)
&& position !== null) {
return command("单窗位置", `window ${target} position ${position}`);
}
}
if (capability === "climate.set_power" && onOff) {
return command("空调电源", `ac ${onOff}`);
}
if (capability === "climate.set_fan_level") {
const level = boundedInteger(parameters.level ?? parameters.fan_level, 1, 7);
if (level !== null) return command("空调风量", `ac fan ${level}`);
}
if (capability === "climate.set_outlet_mode") {
const mode = hyphenated(parameters.mode ?? parameters.outlet_mode);
const modes = new Set(["face", "face-feet", "feet", "feet-defrost", "defrost", "face-feet-defrost", "face-defrost"]);
if (modes.has(mode)) return command("出风模式", `ac mode ${mode}`);
}
if (capability === "climate.set_temperature") {
const temperature = boundedInteger(parameters.temperature_c, 18, 32);
if (new Set(["driver", "passenger", "rear"]).has(target) && temperature !== null) {
return command("空调温度", `ac temp ${target} ${temperature}`);
}
}
if (capability === "climate.set_control_mode") {
const mode = String(parameters.mode ?? parameters.control_mode ?? "");
if (new Set(["manual", "auto"]).has(mode)) return command("空调控制方式", `ac control ${mode}`);
}
if (capability === "climate.set_cycle_mode") {
const mode = String(parameters.mode ?? parameters.cycle_mode ?? "");
if (new Set(["internal", "external"]).has(mode)) return command("空调循环", `ac cycle ${mode}`);
}
if (capability === "outlet.set_position_percent") {
const horizontal = boundedInteger(parameters.horizontal_pct, 0, 100);
const vertical = boundedInteger(parameters.vertical_pct, 0, 100);
if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
&& horizontal !== null && vertical !== null) {
return command("风口百分比", `outlet ${target} percent ${horizontal} ${vertical}`);
}
}
if (capability === "outlet.set_calibrated_position") {
const horizontal = boundedInteger(parameters.horizontal_value, -128, 126);
const vertical = boundedInteger(parameters.vertical_value, -128, 126);
if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
&& horizontal !== null && vertical !== null) {
return command("风口标定坐标", `outlet ${target} position ${horizontal} ${vertical}`);
}
}
if (new Set(["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"]).has(capability)
&& new Set(["driver", "front-left"]).has(target)) {
const feature = capability.includes("heating") ? "heat" : "vent";
const level = boundedInteger(parameters.level, 1, 2);
const setting = onOff === "off" ? "off" : level === 1 ? "high" : level === 2 ? "low" : null;
if (setting) return command("主驾座椅通风/加热", `seat driver ${feature} ${setting}`);
}
if (capability === "seat.set_driver_massage_native"
&& new Set(["driver", "front-left"]).has(target)) {
const action = String(parameters.action || "");
if (action === "off") return command("主驾座椅按摩", "seat driver massage off");
if (action === "strength") {
const strength = { 1: "weak", 2: "medium", 3: "strong" }[Number(parameters.strength)]
|| (new Set(["weak", "medium", "strong"]).has(parameters.strength) ? parameters.strength : null);
if (strength) return command("主驾座椅按摩", `seat driver massage strength ${strength}`);
}
if (action === "mode" && new Set(["wave", "pulse", "stretch", "slack", "relax"]).has(parameters.mode)) {
return command("主驾座椅按摩", `seat driver massage mode ${parameters.mode}`);
}
}
if (capability === "seat.set_driver_axis_native"
&& new Set(["driver", "front-left"]).has(target)) {
const axis = String(parameters.axis || "");
const direction = String(parameters.direction || "");
const validDirections = {
horizontal: new Set(["forward", "backward", "stop"]),
height: new Set(["up", "down", "stop"]),
backrest: new Set(["forward", "backward", "stop"]),
basin: new Set(["up", "down", "stop"]),
};
if (validDirections[axis]?.has(direction)) {
if (direction === "stop") return command("主驾座椅调节", `seat driver ${axis} stop`);
const duration = boundedInteger(parameters.duration_ms, 100, 2000);
if (duration !== null) return command("主驾座椅调节", `seat driver ${axis} ${direction} ${duration}`);
}
}
if (capability === "seat.set_driver_position_native") {
const position = boundedInteger(parameters.position ?? parameters.slot, 1, 3);
if (position !== null) return command("主驾座椅记忆", `seat driver position ${position}`);
}
if (capability === "seat.recall_driver_memory_native") {
const position = boundedInteger(parameters.position ?? parameters.slot, 1, 3);
if (position !== null) return command("主驾座椅记忆", `seat driver memory recall ${position}`);
}
if (capability === "interior_light.set_indoor_power" && onOff) {
return command("室内灯总开关", `light indoor ${onOff}`);
}
if (capability === "interior_light.set_reading_power" && target === "all" && onOff) {
return command("全部阅读灯", `light reading all ${onOff}`);
}
if (capability === "ambient_light.set_power" && onOff) {
return command("氛围灯", `light ambient ${onOff}`);
}
if (capability === "ambient_light.set_color_index") {
const color = boundedInteger(parameters.color_index, 1, 127);
if (color !== null) return command("氛围灯", `light ambient color ${color}`);
}
if (capability === "ambient_light.set_brightness_level") {
const level = boundedInteger(parameters.brightness_level, 1, 5);
if (level !== null) return command("氛围灯", `light ambient brightness ${level}`);
}
if (capability === "roof.set_shade_motion") {
const motion = String(parameters.motion || "");
if (new Set(["open", "close", "half", "stop"]).has(motion)) {
return command("遮阳帘", `shade ${motion}`);
}
}
if (capability === "roof.set_shade_position") {
const position = boundedInteger(parameters.position_pct ?? parameters.shade_position_pct, 0, 100);
if (position !== null) return command("遮阳帘", `shade position ${position}`);
}
return null;
}
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 commands = [];
Object.entries(configurations).forEach(([configKey, configuration]) => {
if (planKeys.size > 0 && !planKeys.has(configKey)) return;
(configuration?.spec?.operations || []).forEach((operation) => {
const item = parsedCanCommandForOperation(operation);
if (item) commands.push(item);
});
});
return [...new Map(commands.map((item) => [`${item.channel}:${item.command}`, item])).values()];
}
function renderParsedCanCommands(result, { message = "" } = {}) {
const panel = $("#v2CanCommandPanel");
const list = $("#v2CanCommandList");
const count = $("#v2CanCommandCount");
if (!panel || !list || !count) return;
const commands = result ? parsedCanCommands(result) : [];
count.textContent = String(commands.length);
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)}">
<span><b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>${escapeHtml(item.category)}</span>
<code>${escapeHtml(item.command)}</code>
${item.frame ? `<small class="v2-can-command-frame">seat-ota · ${escapeHtml(item.frame)}</small>` : ""}
</li>`).join("")}</ol>`;
} else {
list.innerHTML = `<p>${escapeHtml(message || "本次请求未生成已验证的 CAN 控制指令")}</p>`;
}
requestAnimationFrame(syncSidebarLogBaseline);
} }
function closeReasoningPicker() { function closeReasoningPicker() {
@@ -105,7 +355,6 @@
trigger.setAttribute("aria-expanded", "false"); trigger.setAttribute("aria-expanded", "false");
}; };
const open = () => { const open = () => {
closeCustomDropdowns();
reasoningField.hidden = false; reasoningField.hidden = false;
trigger.classList.add("open"); trigger.classList.add("open");
trigger.setAttribute("aria-expanded", "true"); trigger.setAttribute("aria-expanded", "true");
@@ -142,223 +391,43 @@
sync(); sync();
} }
function enhanceSelect(select) {
if (!select || dropdownControls.has(select)) return;
const shell = select.parentElement;
if (!shell) return;
shell.classList.add("v2-custom-select");
select.classList.add("v2-native-select");
const trigger = make("button", "v2-dropdown-trigger", `
<span class="v2-dropdown-value"></span>
<span class="v2-dropdown-caret" aria-hidden="true"></span>`);
trigger.type = "button";
trigger.setAttribute("aria-haspopup", "listbox");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", select.getAttribute("aria-label") || "打开下拉菜单");
const menu = make("div", "v2-dropdown-menu", "");
menu.id = `${select.id}Menu`;
menu.hidden = true;
menu.setAttribute("role", "listbox");
menu.setAttribute("aria-label", select.getAttribute("aria-label") || "选项");
trigger.setAttribute("aria-controls", menu.id);
shell.append(trigger, menu);
const valueNode = $(".v2-dropdown-value", trigger);
const render = () => {
const selected = select.selectedOptions[0] || select.options[0];
valueNode.textContent = selected?.textContent?.trim() || "请选择";
trigger.disabled = select.disabled;
menu.innerHTML = "";
[...select.options].forEach((option) => {
const item = make("button", "v2-dropdown-option", "");
item.type = "button";
item.dataset.value = option.value;
item.textContent = option.textContent.trim();
item.disabled = option.disabled;
item.setAttribute("role", "option");
item.setAttribute("aria-selected", String(option.value === select.value));
menu.append(item);
});
};
const close = () => {
menu.hidden = true;
shell.classList.remove("v2-dropdown-open");
trigger.setAttribute("aria-expanded", "false");
};
const open = () => {
if (trigger.disabled) return;
closeCustomDropdowns();
render();
menu.hidden = false;
shell.classList.add("v2-dropdown-open");
trigger.setAttribute("aria-expanded", "true");
};
const control = { close, menu, open, select, shell, sync: render, trigger };
dropdownControls.set(select, control);
trigger.addEventListener("click", (event) => {
event.stopPropagation();
if (menu.hidden) open();
else close();
});
trigger.addEventListener("keydown", (event) => {
if (event.key === "ArrowDown") {
event.preventDefault();
open();
requestAnimationFrame(() => {
$(".v2-dropdown-option[aria-selected=\"true\"]:not(:disabled)", menu)?.focus();
});
} else if (event.key === "Escape") {
close();
}
});
menu.addEventListener("click", (event) => {
event.stopPropagation();
const item = event.target.closest(".v2-dropdown-option");
if (!item || item.disabled) return;
select.value = item.dataset.value;
select.dispatchEvent(new Event("change", { bubbles: true }));
render();
close();
trigger.focus();
});
menu.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
event.preventDefault();
close();
trigger.focus();
});
select.addEventListener("change", render);
new MutationObserver(render).observe(select, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
render();
}
function enhanceConfiguredDropdowns() {
[$("#vehicleSelect")]
.filter(Boolean)
.forEach(enhanceSelect);
}
function buildSidebar(commandPanel) { function buildSidebar(commandPanel) {
commandPanel.classList.add("v2-sidebar"); commandPanel.classList.add("v2-sidebar");
commandPanel.innerHTML = ""; commandPanel.innerHTML = "";
const profile = make("section", "v2-profile", ` const sidebarToggle = make("button", "v2-sidebar-toggle", '<i data-lucide="panel-left-close"></i>');
<div class="v2-avatar" aria-hidden="true"><span></span></div> sidebarToggle.id = "v2SidebarToggle";
<div class="v2-profile-copy"> sidebarToggle.type = "button";
<small>WELCOME TO THE CABIN</small> sidebarToggle.setAttribute("aria-label", "折叠左侧控制台");
<div class="v2-role-select"> sidebarToggle.setAttribute("aria-expanded", "true");
<span>Hi,</span><strong>user</strong>
</div>
</div>
<button class="v2-sidebar-toggle" id="v2SidebarToggle" type="button" aria-label="折叠左侧控制台" aria-expanded="true">
<i data-lucide="panel-left-close"></i>
</button>`);
const control = make("section", "v2-sidebar-section v2-control-section", `
<div class="v2-section-heading">
<span>CONTROL DESK</span>
<h2>控制台</h2>
</div>
<span class="v2-field-caption">控制方式</span>
<div class="v2-control-switch" role="group" aria-label="控制方式">
<button type="button" data-v2-control="natural"><i data-lucide="message-square-text"></i><span>自然语言控制</span></button>
<button type="button" data-v2-control="gesture"><i data-lucide="scan-face"></i><span>手势控制</span></button>
</div>`);
const config = make("section", "v2-sidebar-section v2-config-section", `
<div class="v2-divider"></div>
<span class="v2-field-caption">配置信息</span>
<div class="v2-config-stack" id="v2ConfigStack"></div>`);
const footer = make("footer", "v2-sidebar-footer", ` const footer = make("footer", "v2-sidebar-footer", `
<span class="v2-live-dot"></span> <span class="v2-live-dot"></span>
<div><strong>智舱控制链路</strong><small id="v2ConnectionText">正在连接服务</small></div> <div><strong>智舱控制链路</strong><small id="v2ConnectionText">正在连接服务</small></div>
<button type="button" id="v2LogoutButton" title="退出登录" aria-label="退出登录"><i data-lucide="log-out"></i></button>`); <button type="button" id="v2LogoutButton" title="退出登录" aria-label="退出登录"><i data-lucide="log-out"></i></button>`);
commandPanel.append(profile, control, config, footer); commandPanel.append(sidebarToggle, footer);
return $("#v2ConfigStack");
} }
function buildConfigFields(configStack, originalForm) { function prepareRuntimeControls(originalForm) {
const vehicleField = $(".vehicle-field", originalForm); const vehicleField = $(".vehicle-field", originalForm);
const transportField = $("#vehicleTransportField", originalForm);
const providerField = $(".field-group:has(.segmented [data-provider])", originalForm); const providerField = $(".field-group:has(.segmented [data-provider])", originalForm);
const reasoningField = $("#reasoningField", originalForm); const reasoningField = $("#reasoningField", originalForm);
const executionField = $(".field-group:has(.execution-mode-options)", originalForm); const executionField = $(".field-group:has(.execution-mode-options)", originalForm);
const simulationFaultField = $("#simulationFaultField", originalForm); const simulationFaultField = $("#simulationFaultField", originalForm);
if (vehicleField) { transportField?.classList.add("v2-transport-field");
vehicleField.classList.add("v2-config-field", "v2-vehicle-field"); executionField?.classList.add("v2-execution-field");
const vehicleControl = $(".vehicle-select-control", vehicleField); simulationFaultField?.classList.add("v2-simulation-fault-field");
const vehicleSelect = $("#vehicleSelect", vehicleField);
const vehicleIcon = $("svg, [data-lucide]", vehicleControl);
if (vehicleControl && vehicleSelect && vehicleIcon) {
const copy = make("div", "v2-config-card-copy", "<small>车型配置</small>");
const selectSlot = make("div", "v2-config-card-select", "");
const state = make("span", "v2-field-state", "CHECK");
state.id = "v2VehicleState";
selectSlot.append(vehicleSelect);
copy.append(selectSlot);
vehicleControl.classList.add("v2-config-card", "v2-config-select-card");
vehicleControl.id = "v2VehicleCard";
vehicleControl.dataset.state = "checking";
vehicleControl.replaceChildren(vehicleIcon, copy, state);
$(".field-label", vehicleField)?.remove();
}
configStack.append(vehicleField);
}
const modelField = make("div", "v2-config-field v2-config-card v2-model-field", `
<i data-lucide="sparkles"></i>
<div><small>推理模型</small><strong id="v2ModelValue">模型检测中</strong></div>
<span class="v2-field-state" id="v2ModelState">AUTO</span>
<p class="provider-note" id="v2ModelNote">模型由服务端安全配置</p>`);
modelField.id = "v2ModelField";
configStack.append(modelField);
installReasoningPicker(reasoningField, originalForm); installReasoningPicker(reasoningField, originalForm);
const gestureModelField = make("div", "v2-config-field v2-config-card v2-gesture-model-field", ` [vehicleField, providerField].forEach((field) => {
<i data-lucide="scan-line"></i>
<div><small>手势模型</small><strong id="v2GestureModelValue">MediaPipe GR</strong></div>
<span class="v2-field-state">VISION</span>
<small class="v2-gesture-model-note" id="v2GestureModelNote">Tasks Vision · 0.10.14</small>`);
gestureModelField.id = "v2GestureModelField";
configStack.append(gestureModelField);
const userHardwareState = make("div", "v2-user-target-status", `
<i></i><span id="v2UserTargetText">正在准备虚拟 CAN</span>`);
configStack.append(userHardwareState);
if (providerField) {
providerField.classList.add("v2-provider-field");
providerField.hidden = true;
configStack.append(providerField);
}
if (executionField) {
executionField.classList.add("v2-native-execution-field");
executionField.hidden = true;
configStack.append(executionField);
}
const advanced = make("div", "v2-advanced-stack", "");
[simulationFaultField].forEach((field) => {
if (!field) return; if (!field) return;
field.classList.add("v2-config-field", "v2-advanced-field"); field.classList.add("v2-runtime-data-field");
advanced.append(field); field.hidden = true;
}); });
configStack.append(advanced);
} }
function buildOperation(mainPanel, originalForm, panelHeading, modeRail, requestHeader, gestureDrawer) { function buildOperation(mainPanel, originalForm, panelHeading, modeRail, requestHeader, gestureDrawer) {
@@ -576,16 +645,14 @@
const syncedDevices = [ const syncedDevices = [
{ {
id: "passenger", name: "副驾座椅", icon: "armchair", id: "passenger", name: "副驾座椅", icon: "armchair",
rows: () => [["通风", seatFunctionValue("passenger", "ventilation")], ["加热", seatFunctionValue("passenger", "heating")], ["水平", seatAxisValue("passenger", "longitudinal")], ["坐垫", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]], rows: () => [["前后", seatAxisValue("passenger", "longitudinal")], ["高度", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]],
controls: () => [ controls: () => [
selectControl("ventilation", "通风", seatLevelControlValue("passenger", "ventilation"), levelOptions), rangeControl("longitudinal", "前后", seatAxisValue("passenger", "longitudinal")),
selectControl("heating", "加热", seatLevelControlValue("passenger", "heating"), levelOptions), rangeControl("cushion_tilt", "高度", seatAxisValue("passenger", "cushion_tilt")),
rangeControl("longitudinal", "水平", seatAxisValue("passenger", "longitudinal")),
rangeControl("cushion_tilt", "坐垫", seatAxisValue("passenger", "cushion_tilt")),
rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")), rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")),
rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")), rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")),
], ],
command: (values) => `调节副驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},水平${values.longitudinal}%,坐垫${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`, command: (values) => `调节副驾座椅:前后${values.longitudinal}%,座椅高度${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`,
}, },
{ {
id: "driver", name: "主驾座椅", icon: "armchair", id: "driver", name: "主驾座椅", icon: "armchair",
@@ -1000,6 +1067,11 @@
button.classList.toggle("v2-sync-active", active); button.classList.toggle("v2-sync-active", active);
}); });
renderTaskTimeline(task); renderTaskTimeline(task);
if (normalizedStatus === "running") {
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
} else if (result) {
renderParsedCanCommands(result);
}
} }
function applyDisplaySnapshot(snapshot) { function applyDisplaySnapshot(snapshot) {
@@ -1021,9 +1093,6 @@
: listening ? "等待 USB 外设" : "扩展链路检查中"; : listening ? "等待 USB 外设" : "扩展链路检查中";
usbState.dataset.state = connected ? "online" : listening ? "listening" : "offline"; usbState.dataset.state = connected ? "online" : listening ? "listening" : "offline";
} }
if ($("#v2SyncTransport")) {
$("#v2SyncTransport").textContent = `${String(health.transport || "GSCU").toUpperCase()} · ${health.peer || health.listen || "等待连接"}`;
}
const vehicleOnline = health.vehicle?.connected !== false; const vehicleOnline = health.vehicle?.connected !== false;
const vehicleBadge = $("#v2HeadUnitState"); const vehicleBadge = $("#v2HeadUnitState");
if (vehicleBadge) { if (vehicleBadge) {
@@ -1118,7 +1187,26 @@
const sidebar = $(".v2-sidebar"); const sidebar = $(".v2-sidebar");
const lowerPanel = $("#v2DevicePanel") || $(".joint-twin-shell > .twin-band"); const lowerPanel = $("#v2DevicePanel") || $(".joint-twin-shell > .twin-band");
if (!sidebar || !lowerPanel) return; if (!sidebar || !lowerPanel) return;
const offset = Math.max(0, lowerPanel.getBoundingClientRect().top - sidebar.getBoundingClientRect().top); const sidebarRect = sidebar.getBoundingClientRect();
const overview = $(".v2-sync-sidebar-overview", sidebar);
const overviewRect = overview?.getBoundingClientRect();
const overviewBottom = overview && overviewRect
? Array.from(overview.children).reduce(
(bottom, child) => Math.max(bottom, child.getBoundingClientRect().bottom),
overviewRect.top,
)
: 0;
const overviewPaddingBottom = overview
? Number.parseFloat(getComputedStyle(overview).paddingBottom) || 0
: 0;
const requiredOverviewHeight = overviewRect
? Math.ceil(overviewBottom - overviewRect.top + overviewPaddingBottom + 10)
: 0;
const offset = Math.max(
0,
lowerPanel.getBoundingClientRect().top - sidebarRect.top,
requiredOverviewHeight,
);
sidebar.style.setProperty("--v2-sync-log-top", `${offset}px`); sidebar.style.setProperty("--v2-sync-log-top", `${offset}px`);
} }
@@ -1137,7 +1225,6 @@
<div class="v2-sync-link" id="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div> <div class="v2-sync-link" id="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div>
<div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div> <div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div>
</div> </div>
<div class="v2-sync-protocol"><i data-lucide="radio-tower"></i><div><small>扩展链路</small><strong id="v2SyncTransport">GSCU · 等待连接</strong></div></div>
</div> </div>
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle"> <section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
<header><div><small>RUNTIME / SYNC</small><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header> <header><div><small>RUNTIME / SYNC</small><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
@@ -1145,7 +1232,39 @@
</section>`); </section>`);
if (sidebar && sidebarFooter) { if (sidebar && sidebarFooter) {
sidebar.insertBefore(syncSidebar, sidebarFooter); sidebar.insertBefore(syncSidebar, sidebarFooter);
$(".v2-sync-sidebar-overview", syncSidebar)?.append(sidebarFooter); const syncOverview = $(".v2-sync-sidebar-overview", syncSidebar);
const transportField = $("#vehicleTransportField");
const executionField = $(".field-group:has(.execution-mode-options)");
const simulationFaultField = $("#simulationFaultField");
if (syncOverview) {
const linkStack = $(".v2-sync-link-stack", syncOverview);
const insertVisibleControl = (field, className) => {
if (!field) return;
field.classList.add(className);
if (linkStack) syncOverview.insertBefore(field, linkStack);
else syncOverview.append(field);
};
insertVisibleControl(transportField, "v2-sync-transport-field");
if (executionField) executionField.hidden = false;
insertVisibleControl(executionField, "v2-sync-execution-field");
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>
<strong id="v2CanCommandCount">0</strong>
<i data-lucide="chevron-down"></i>
</summary>
<div class="v2-can-command-list" id="v2CanCommandList">
<p>等待自然语言解析结果</p>
</div>`);
canCommandPanel.id = "v2CanCommandPanel";
if (linkStack) syncOverview.insertBefore(canCommandPanel, linkStack);
else syncOverview.append(canCommandPanel);
canCommandPanel.addEventListener("toggle", () => {
requestAnimationFrame(syncSidebarLogBaseline);
});
}
syncOverview?.append(sidebarFooter);
const sidebarToggle = $("#v2SidebarToggle", sidebar); const sidebarToggle = $("#v2SidebarToggle", sidebar);
if (sidebarToggle) $(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle); if (sidebarToggle) $(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);
} }
@@ -1228,8 +1347,12 @@
if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target); if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target);
}); });
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs); $("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
$("#requestForm")?.addEventListener("submit", () => {
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
});
window.addEventListener("cognitive-agent:result", (event) => { window.addEventListener("cognitive-agent:result", (event) => {
const result = event.detail || {}; const result = event.detail || {};
renderParsedCanCommands(result);
appendSyncLog({ appendSyncLog({
key: result.request_id ? `result:${result.request_id}` : "", key: result.request_id ? `result:${result.request_id}` : "",
category: "task", category: "task",
@@ -1253,6 +1376,7 @@
renderSyncedDevices(); renderSyncedDevices();
} }
if (event.detail?.error) { if (event.detail?.error) {
renderParsedCanCommands(null, { message: "本次请求未生成 CAN 控制指令" });
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" }); appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" });
} }
}); });
@@ -1370,23 +1494,18 @@
if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) { if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) {
statusTarget.append(requestStatus); statusTarget.append(requestStatus);
} }
$$('[data-v2-control]').forEach((button) => {
const active = button.dataset.v2Control === ui.controlMode;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
requestAnimationFrame(updateModeEdgeHints); requestAnimationFrame(updateModeEdgeHints);
const drawer = $("#gestureDrawer"); const drawer = $("#gestureDrawer");
if (!triggerGesture) { if (!triggerGesture) {
const gestureVisible = ui.controlMode === "gesture"; const gestureVisible = ui.controlMode === "gesture";
drawer?.setAttribute("aria-hidden", String(!gestureVisible)); drawer?.setAttribute("aria-hidden", String(!gestureVisible));
$("#gestureToggleButton")?.setAttribute("aria-expanded", String(gestureVisible)); drawer?.classList.toggle("open", gestureVisible);
return; return;
} }
if (ui.controlMode === "gesture") { if (ui.controlMode === "gesture") {
if (!drawer?.classList.contains("open")) $("#gestureToggleButton")?.click(); window.CognitiveAgentGesture?.open?.();
} else if (drawer?.classList.contains("open")) { } else if (drawer?.classList.contains("open")) {
$("#gestureCloseButton")?.click(); window.CognitiveAgentGesture?.close?.();
} }
} }
@@ -1435,120 +1554,53 @@
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180); window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
} }
function requestUserSafeExecutionMode() { function keepRunButtonAvailable() {
const dryRunButton = $('[data-execution-mode="dry_run"]');
const note = $("#v2ExecutionNote");
const userTarget = $(".v2-user-target-status");
const userTargetText = $("#v2UserTargetText");
const runButton = $("#runButton"); const runButton = $("#runButton");
if (!dryRunButton) return;
if (!dryRunButton.classList.contains("active")) dryRunButton.click();
if (note) note.textContent = "user 固定使用虚拟 CAN 安全执行环境";
if (userTarget) userTarget.dataset.state = "ready";
if (userTargetText) userTargetText.textContent = "执行目标:虚拟 CAN";
if (runButton && !runButton.classList.contains("loading")) runButton.disabled = false; if (runButton && !runButton.classList.contains("loading")) runButton.disabled = false;
syncExecutionSelector();
} }
function syncExecutionSelector() { function syncRuntimePresentation(connectionState = window.CognitiveAgentNetwork?.getState?.()) {
const select = $("#v2ExecutionSelect");
if (!select) return;
const active = $('[data-execution-mode].active');
if (active) select.value = active.dataset.executionMode;
["dry_run", "simulated_hardware", "hardware"].forEach((value) => {
const option = $(`option[value="${value}"]`, select);
const button = $(`[data-execution-mode="${value}"]`);
if (option && button) option.disabled = button.disabled;
});
dropdownControls.get(select)?.sync();
}
function syncRuntimePresentation() {
const modelBadge = $("#modelBadge");
const connectionBadge = $("#connectionBadge");
const modelValue = $("#v2ModelValue");
const modelNote = $("#v2ModelNote");
const connectionText = $("#v2ConnectionText"); const connectionText = $("#v2ConnectionText");
if (modelBadge && modelValue) { if (connectionText) {
const text = modelBadge.textContent.trim() || "模型检测中"; const status = connectionState?.status || "connecting";
modelValue.textContent = text; const labels = {
const unavailable = modelBadge.classList.contains("danger"); connecting: "正在连接服务",
$("#v2ModelField")?.classList.toggle("unavailable", unavailable); online: "服务已连接",
$("#v2ModelState").textContent = unavailable ? "LOCAL" : "READY"; degraded: "连接不稳定",
if (modelNote) modelNote.textContent = unavailable offline: "服务已断开",
? "GPT 不可用,自动使用本地规则" };
: "服务端安全托管 · 失败自动降级"; connectionText.textContent = labels[status] || labels.connecting;
} connectionText.title = connectionState?.lastError || connectionText.textContent;
if (connectionBadge && connectionText) { $(".v2-sidebar-footer")?.classList.toggle(
connectionText.textContent = connectionBadge.textContent.trim() || "正在连接服务"; "offline",
const online = !connectionBadge.classList.contains("danger") status === "offline" || status === "degraded",
&& !connectionBadge.classList.contains("warning"); );
$(".v2-sidebar-footer")?.classList.toggle("offline", !online);
}
syncExecutionSelector();
requestUserSafeExecutionMode();
}
function setVehicleLinkState(connected) {
const card = $("#v2VehicleCard");
const state = $("#v2VehicleState");
if (!card || !state) return;
card.dataset.state = connected ? "online" : "offline";
state.textContent = connected ? "READY" : "OFFLINE";
}
async function refreshVehicleLinkState() {
const card = $("#v2VehicleCard");
const state = $("#v2VehicleState");
if (card) card.dataset.state = "checking";
if (state) state.textContent = "CHECK";
try {
const response = await fetch("/api/extension/health", {
credentials: "same-origin",
headers: { Accept: "application/json" },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const health = await response.json();
setVehicleLinkState(Boolean(health.vehicle?.connected));
} catch (_error) {
setVehicleLinkState(false);
} }
keepRunButtonAvailable();
} }
function bindEvents() { function bindEvents() {
$$('[data-v2-control]').forEach((button) => {
button.addEventListener("click", () => {
setControlMode(button.dataset.v2Control);
});
});
$$('.v2-module-tab').forEach((button) => { $$('.v2-module-tab').forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
if (!button.disabled) setModule(button.dataset.module); if (!button.disabled) setModule(button.dataset.module);
}); });
}); });
$("#v2ExecutionSelect")?.addEventListener("change", (event) => {
$(`[data-execution-mode="${event.target.value}"]`)?.click();
syncExecutionSelector();
});
$$(".v2-page-button").forEach((button) => { $$(".v2-page-button").forEach((button) => {
button.addEventListener("click", () => setPage(button.dataset.v2PageTarget)); button.addEventListener("click", () => setPage(button.dataset.v2PageTarget));
}); });
$("#v2LogoutButton")?.addEventListener("click", () => $("#logoutButton")?.click()); $("#v2LogoutButton")?.addEventListener(
window.addEventListener("cognitive-agent:connection", (event) => { "click",
if (event.detail?.status === "online") refreshVehicleLinkState(); () => window.CognitiveAgentConsole?.logout?.(),
else if (["offline", "degraded"].includes(event.detail?.status)) setVehicleLinkState(false); );
});
$("#v2SidebarToggle")?.addEventListener( $("#v2SidebarToggle")?.addEventListener(
"click", "click",
() => setSidebarCollapsed(!ui.sidebarCollapsed), () => setSidebarCollapsed(!ui.sidebarCollapsed),
); );
document.addEventListener("click", () => { document.addEventListener("click", () => {
closeCustomDropdowns();
closeReasoningPicker(); closeReasoningPicker();
}); });
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape") {
closeCustomDropdowns();
closeReasoningPicker(); closeReasoningPicker();
clearTwinDevicePreview({ force: true }); clearTwinDevicePreview({ force: true });
} }
@@ -1577,8 +1629,11 @@
.observe(modeChips, { childList: true }); .observe(modeChips, { childList: true });
} }
const observer = new MutationObserver(syncRuntimePresentation); window.addEventListener("cognitive-agent:connection", (event) => {
[$("#modelBadge"), $("#connectionBadge"), $("#hardwareMode"), $("#simulationMode")] syncRuntimePresentation(event.detail);
});
const observer = new MutationObserver(() => keepRunButtonAvailable());
[$("#hardwareMode"), $("#simulationMode")]
.filter(Boolean) .filter(Boolean)
.forEach((node) => observer.observe(node, { .forEach((node) => observer.observe(node, {
attributes: true, attributes: true,
@@ -1598,8 +1653,6 @@
document.body.classList.add("console-v2"); document.body.classList.add("console-v2");
const panelHeading = $(".panel-heading", commandPanel); const panelHeading = $(".panel-heading", commandPanel);
const modeRail = $(".mode-rail", commandPanel); const modeRail = $(".mode-rail", commandPanel);
const exampleGroup = $(".example-group", commandPanel);
const safetyStrip = $("#safetyStrip", commandPanel);
const requestHeader = $(".request-header", mainPanel); const requestHeader = $(".request-header", mainPanel);
const gestureDrawer = $("#gestureDrawer"); const gestureDrawer = $("#gestureDrawer");
@@ -1609,18 +1662,11 @@
requestHeader, requestHeader,
gestureDrawer, gestureDrawer,
originalForm, originalForm,
exampleGroup,
safetyStrip,
}; };
Object.values(detached).filter(Boolean).forEach((node) => node.remove()); Object.values(detached).filter(Boolean).forEach((node) => node.remove());
const legacyParking = make("div", "v2-legacy-parking v2-legacy-hidden", ""); buildSidebar(commandPanel);
[exampleGroup, safetyStrip].filter(Boolean).forEach((node) => legacyParking.append(node)); prepareRuntimeControls(originalForm);
document.body.append(legacyParking);
const configStack = buildSidebar(commandPanel);
buildConfigFields(configStack, originalForm);
enhanceConfiguredDropdowns();
buildOperation( buildOperation(
mainPanel, mainPanel,
originalForm, originalForm,
@@ -1630,9 +1676,6 @@
gestureDrawer, gestureDrawer,
); );
buildModuleSwitcher(mainPanel); buildModuleSwitcher(mainPanel);
$(".pipeline", mainPanel)?.classList.add("v2-legacy-hidden");
$(".topbar")?.classList.add("v2-legacy-hidden");
$(".gesture-backdrop")?.classList.add("v2-legacy-hidden");
buildOutput(mainPanel); buildOutput(mainPanel);
buildSyncControlLayer(); buildSyncControlLayer();
bindEvents(); bindEvents();
@@ -1641,18 +1684,18 @@
setSidebarCollapsed(ui.sidebarCollapsed); setSidebarCollapsed(ui.sidebarCollapsed);
setPage(0); setPage(0);
syncRuntimePresentation(); syncRuntimePresentation();
refreshVehicleLinkState();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
requestAnimationFrame(() => { requestAnimationFrame(() => {
updateModeEdgeHints(); updateModeEdgeHints();
document.body.classList.add("v2-ready"); document.body.classList.add("v2-ready");
window.setTimeout(() => $("#v2BootScreen")?.remove(), 220);
}); });
} }
window.CognitiveAgentConsoleV2 = { window.CognitiveAgentConsoleV2 = {
enhanceSelect,
getModule: () => ui.activeModule, getModule: () => ui.activeModule,
getRole: () => "user", getRole: () => "user",
getParsedCanCommands: (result) => parsedCanCommands(result).map((item) => ({ ...item })),
setModule, setModule,
}; };
+35 -13
View File
@@ -56,6 +56,8 @@ const state = {
customizeOpen: false, customizeOpen: false,
engine: null, engine: null,
runtimeReady: false, runtimeReady: false,
enabled: true,
disabledReason: "",
cameraStatus: "checking", cameraStatus: "checking",
cameraMessage: "正在检测摄像头…", cameraMessage: "正在检测摄像头…",
cameraCheckSequence: 0, cameraCheckSequence: 0,
@@ -114,7 +116,8 @@ function cameraFailureDetails(error) {
function syncCameraButtonAvailability() { function syncCameraButtonAvailability() {
const button = $("#gestureStartButton"); const button = $("#gestureStartButton");
if (!button || state.stream) return; if (!button || state.stream) return;
button.disabled = !state.runtimeReady button.disabled = !state.enabled
|| !state.runtimeReady
|| ["checking", "unavailable", "unsupported"].includes(state.cameraStatus); || ["checking", "unavailable", "unsupported"].includes(state.cameraStatus);
} }
@@ -693,29 +696,48 @@ function resetMapping() {
} }
function openDrawer() { function openDrawer() {
$("#gestureDrawer").classList.add("open"); if (!state.enabled) {
$("#gestureDrawer").setAttribute("aria-hidden", "false"); notify(state.disabledReason || "当前运行模式未启用手势控制");
$("#gestureBackdrop").hidden = false; return false;
requestAnimationFrame(() => $("#gestureBackdrop").classList.add("visible")); }
$("#gestureToggleButton").setAttribute("aria-expanded", "true"); const drawer = $("#gestureDrawer");
if (!drawer) return false;
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.classList.add("gesture-open"); document.body.classList.add("gesture-open");
return true;
} }
function closeDrawer() { function closeDrawer() {
stopCamera(); stopCamera();
$("#gestureDrawer").classList.remove("open"); const drawer = $("#gestureDrawer");
$("#gestureDrawer").setAttribute("aria-hidden", "true"); drawer?.classList.remove("open");
$("#gestureBackdrop").classList.remove("visible"); drawer?.setAttribute("aria-hidden", "true");
window.setTimeout(() => { $("#gestureBackdrop").hidden = true; }, 240);
$("#gestureToggleButton").setAttribute("aria-expanded", "false");
document.body.classList.remove("gesture-open"); document.body.classList.remove("gesture-open");
} }
function setEnabled(enabled, { reason = "" } = {}) {
state.enabled = Boolean(enabled);
state.disabledReason = state.enabled ? "" : String(reason || "当前运行模式未启用手势控制");
if (!state.enabled) closeDrawer();
syncCameraButtonAvailability();
const drawer = $("#gestureDrawer");
if (drawer) {
drawer.dataset.enabled = String(state.enabled);
drawer.title = state.disabledReason;
}
}
window.CognitiveAgentGesture = {
open: openDrawer,
close: closeDrawer,
isOpen: () => $("#gestureDrawer")?.classList.contains("open") === true,
setEnabled,
};
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
$("#gestureStartButton").disabled = true; $("#gestureStartButton").disabled = true;
$("#gestureToggleButton")?.addEventListener("click", openDrawer);
$("#gestureCloseButton")?.addEventListener("click", closeDrawer); $("#gestureCloseButton")?.addEventListener("click", closeDrawer);
$("#gestureBackdrop")?.addEventListener("click", closeDrawer);
$("#gestureStartButton")?.addEventListener("click", toggleCamera); $("#gestureStartButton")?.addEventListener("click", toggleCamera);
$("#gestureStopButton")?.addEventListener("click", () => state.engine?.setArmed(false)); $("#gestureStopButton")?.addEventListener("click", () => state.engine?.setArmed(false));
$("#gestureCustomizeButton")?.addEventListener("click", toggleCustomize); $("#gestureCustomizeButton")?.addEventListener("click", toggleCustomize);
+99 -120
View File
@@ -9,44 +9,26 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/app-icon-192.png" /> <link rel="icon" href="/icons/app-icon-192.png" />
<title>511工程-智能座舱</title> <title>511工程-智能座舱</title>
<link rel="stylesheet" href="/styles.css?v=dual-seat-9" /> <link rel="stylesheet" href="/styles.css?v=dual-seat-16" />
<link rel="stylesheet" href="/console-v2.css?v=sync-layout-26" /> <link rel="stylesheet" href="/console-v2.css?v=sync-layout-40" />
<link rel="stylesheet" href="/self-extension.css?v=sync-layout-26" /> <link rel="stylesheet" href="/self-extension.css?v=sync-layout-26" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script> <script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" defer></script> <script src="/vendor/three/0.147.0/three.min.js" defer></script>
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script> <script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script> <script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-1" defer></script> <script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-85" defer></script> <script src="/app.js?v=dual-seat-101" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-8"></script> <script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-2" defer></script> <script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=sync-layout-26" defer></script> <script src="/console-v2.js?v=sync-layout-41" defer></script>
<script src="/self-extension.js?v=sync-layout-26" defer></script> <script src="/self-extension.js?v=sync-layout-26" defer></script>
</head> </head>
<body> <body class="console-v2">
<header class="topbar"> <div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite">
<div class="brand-block"> <span class="v2-boot-mark" aria-hidden="true"><i></i><i></i><i></i></span>
<span class="brand-mark" aria-hidden="true"><i data-lucide="brain-circuit"></i></span> <strong>511工程-智能座舱</strong>
<div> <small>正在加载车机控制与实时状态</small>
<h1>Cognitive Agent</h1> </div>
<p>CABIN ORCHESTRATION <span>/</span> MULTI-DOMAIN</p>
</div>
</div>
<div class="runtime-badges" aria-label="运行环境">
<button class="gesture-entry" id="gestureToggleButton" type="button" aria-controls="gestureDrawer" aria-expanded="false">
<span class="gesture-entry-dot"></span><i data-lucide="scan-face"></i>手势控制
</button>
<span class="status-badge neutral connection-badge" id="connectionBadge"><i data-lucide="loader-circle"></i><span>连接中</span></span>
<button class="status-badge neutral pwa-badge" id="pwaBadge" type="button" hidden></button>
<span class="status-badge neutral" id="executionBadge"><i data-lucide="flask-conical"></i>DRY-RUN</span>
<span class="status-badge" id="modelBadge"><i data-lucide="sparkles"></i>模型检测中</span>
<span class="status-badge danger" id="hardwareBadge"><i data-lucide="shield-off"></i>硬件检测中</span>
<button class="status-badge auth-logout" id="logoutButton" type="button" title="退出登录" aria-label="退出登录" hidden>
<i data-lucide="log-out"></i>
</button>
</div>
</header>
<div class="workspace"> <div class="workspace">
<aside class="command-panel"> <aside class="command-panel">
<div class="panel-heading"> <div class="panel-heading">
@@ -126,10 +108,10 @@
<div class="field-group"> <div class="field-group">
<span class="field-label">执行目标</span> <span class="field-label">执行目标</span>
<div class="segmented execution-mode-options" role="group" aria-label="执行目标"> <div class="segmented execution-mode-options" role="group" aria-label="执行目标">
<button type="button" class="mode-option active" data-execution-mode="dry_run"> <button type="button" class="mode-option" data-execution-mode="dry_run">
<i data-lucide="flask-conical"></i>虚拟 CAN <i data-lucide="flask-conical"></i>虚拟 CAN
</button> </button>
<button type="button" class="mode-option" id="hardwareMode" data-execution-mode="hardware" disabled> <button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware" disabled>
<i data-lucide="radio-tower"></i>真实台架 <i data-lucide="radio-tower"></i>真实台架
</button> </button>
<button type="button" class="mode-option" id="simulationMode" data-execution-mode="simulated_hardware" disabled> <button type="button" class="mode-option" id="simulationMode" data-execution-mode="simulated_hardware" disabled>
@@ -140,6 +122,19 @@
<p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p> <p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p>
</div> </div>
<div class="field-group" id="vehicleTransportField">
<span class="field-label">车辆连接方式</span>
<div class="segmented vehicle-transport-options" role="group" aria-label="车辆连接方式">
<button type="button" class="mode-option active" data-control-transport="wired_can">
<i data-lucide="cable"></i>有线 CAN
</button>
<button type="button" class="mode-option" data-control-transport="wireless_mqtt">
<i data-lucide="wifi"></i>无线 CAN
</button>
</div>
<p class="provider-note" id="vehicleTransportNote">正在检查车辆连接</p>
</div>
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden> <div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
<label class="field-label" for="simulationFault">故障注入场景</label> <label class="field-label" for="simulationFault">故障注入场景</label>
<select id="simulationFault" aria-label="选择模拟硬件故障"> <select id="simulationFault" aria-label="选择模拟硬件故障">
@@ -170,47 +165,6 @@
</button> </button>
</form> </form>
<div class="example-group">
<span class="field-label">示例请求</span>
<button type="button" class="example-command featured-example" data-example="副驾座椅通风3档,同时打开空调,温度设为24度,风量3档">
<i data-lucide="workflow"></i><span>副驾通风 + 空调 24℃ · 双通道联动</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="空调打开,车内温度调到24度,风量3档">
<i data-lucide="fan"></i><span>空调 24℃ · 风量 3 档</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="天幕透光率调到30%,遮阳帘打开一半">
<i data-lucide="panels-top-left"></i><span>天幕透光 30% · 遮阳帘 50%</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="打开氛围灯,调成冰蓝色,亮度60%">
<i data-lucide="lamp-ceiling"></i><span>冰蓝氛围灯 · 亮度 60%</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command featured-example" data-example="进入观影状态:副驾调整为舒适观影姿态,空调24度、风量2档,天幕调暗,氛围灯换成暖橙色">
<i data-lucide="clapperboard"></i><span>跨域观影场景</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="副驾有点热,有点闷">
<i data-lucide="wind"></i><span>副驾有点热,有点闷</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="副驾靠背向后移动5度">
<i data-lucide="move-horizontal"></i><span>副驾靠背向后 5°</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="副驾调整为零重力模式,要求靠背座椅夹角128度,坐垫脚托夹角133度">
<i data-lucide="compass"></i><span>副驾零重力精确夹角</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="将当前状态保存为零重力模式">
<i data-lucide="bookmark-plus"></i><span>保存当前座椅姿态</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command warning-example" data-example="副驾座椅加热开到三档">
<i data-lucide="triangle-alert"></i><span>三档请求(档位虚拟)</span><i data-lucide="chevron-right"></i>
</button>
</div>
<div class="safety-strip" id="safetyStrip">
<i data-lucide="shield-check"></i>
<div>
<strong id="safetyTitle">虚拟执行边界</strong>
<span id="safetyText">四域 CAN 均为 dry-run,实车发送被强制关闭。</span>
</div>
</div>
</aside> </aside>
<main class="main-panel"> <main class="main-panel">
@@ -229,20 +183,6 @@
</div> </div>
</section> </section>
<section class="pipeline" aria-label="执行链路">
<div class="pipeline-step" data-stage="intent_parse"><span>01</span><i data-lucide="message-square-text"></i><b>意图抽取</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="function_generate"><span>02</span><i data-lucide="file-code-2"></i><b>配置生成</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="domain_validate"><span>03</span><i data-lucide="badge-check"></i><b>双层校验</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="compile"><span>04</span><i data-lucide="binary"></i><b>CAN 编译</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="safety_check"><span>05</span><i data-lucide="shield-check"></i><b>安全门禁</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="execute"><span>06</span><i data-lucide="radio-tower"></i><b id="executeStageLabel">虚拟执行</b></div>
</section>
<section class="cabin-band" aria-labelledby="cabinStateTitle"> <section class="cabin-band" aria-labelledby="cabinStateTitle">
<div class="section-title-row cabin-heading"> <div class="section-title-row cabin-heading">
<div class="cabin-title-copy"> <div class="cabin-title-copy">
@@ -261,27 +201,27 @@
<header> <header>
<span class="domain-icon"><i data-lucide="fan"></i></span> <span class="domain-icon"><i data-lucide="fan"></i></span>
<div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div> <div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div>
<span class="device-state" id="climatePower">OFF</span> <span class="device-state" id="climatePower">UNKNOWN</span>
</header> </header>
<p class="domain-capability-line"><span>可控参数</span>电源 · 温度 · 风量/风向</p> <p class="domain-capability-line"><span>可控参数</span>电源 · 温度 · 风量/风向</p>
<div class="climate-display"> <div class="climate-display">
<div class="zone-temperature"> <div class="zone-temperature">
<span>主驾</span><strong id="driverTemperature">24<sup>℃</sup></strong> <span>主驾</span><strong id="driverTemperature">--<sup>℃</sup></strong>
<small>风向<b id="driverAirDirection">-</b></small> <small>风向<b id="driverAirDirection">-</b></small>
</div> </div>
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量"> <div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i> <i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div> </div>
<div class="zone-temperature passenger-temperature"> <div class="zone-temperature passenger-temperature">
<span>副驾</span><strong id="passengerTemperature">24<sup>℃</sup></strong> <span>副驾</span><strong id="passengerTemperature">--<sup>℃</sup></strong>
<small>风向<b id="passengerAirDirection">-</b></small> <small>风向<b id="passengerAirDirection">-</b></small>
</div> </div>
</div> </div>
<dl class="domain-readouts"> <dl class="domain-readouts">
<div><dt>风量</dt><dd id="fanLevel">0 档</dd></div> <div><dt>风量</dt><dd id="fanLevel">未知</dd></div>
<div><dt>循环</dt><dd id="cycleMode">外循环</dd></div> <div><dt>循环</dt><dd id="cycleMode">未知</dd></div>
<div><dt>A/C</dt><dd id="acState">关闭</dd></div> <div><dt>A/C</dt><dd id="acState">未知</dd></div>
<div><dt>前除霜</dt><dd id="defrostState">关闭</dd></div> <div><dt>前除霜</dt><dd id="defrostState">未知</dd></div>
</dl> </dl>
</article> </article>
@@ -308,17 +248,17 @@
<header> <header>
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span> <span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
<div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div> <div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div>
<span class="device-state" id="roofState">READY</span> <span class="device-state" id="roofState">UNKNOWN</span>
</header> </header>
<p class="domain-capability-line"><span>可控参数</span>开关 / 停止 · 位置</p> <p class="domain-capability-line"><span>可控参数</span>开关 / 停止 · 位置</p>
<div class="domain-summary-visual shade-summary-visual" aria-hidden="true"> <div class="domain-summary-visual shade-summary-visual" aria-hidden="true">
<span>闭合</span><i><b id="shadePositionMeter"></b></i><span>打开</span> <span>闭合</span><i><b id="shadePositionMeter"></b></i><span>打开</span>
</div> </div>
<dl class="domain-readouts"> <dl class="domain-readouts">
<div><dt>遮阳帘</dt><dd id="shadePositionValue">0%</dd></div> <div><dt>遮阳帘</dt><dd id="shadePositionValue">未知</dd></div>
<div><dt>动作</dt><dd id="shadeMotionState">停止</dd></div> <div><dt>动作</dt><dd id="shadeMotionState">未知</dd></div>
<div><dt>初始化</dt><dd id="shadeInitializedState">未知</dd></div> <div><dt>初始化</dt><dd id="shadeInitializedState">未知</dd></div>
<div><dt>热保护</dt><dd id="shadeThermalState">正常</dd></div> <div><dt>热保护</dt><dd id="shadeThermalState">未知</dd></div>
</dl> </dl>
</article> </article>
@@ -326,7 +266,7 @@
<header> <header>
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span> <span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
<div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div> <div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div>
<span class="device-state" id="ambientPower">OFF</span> <span class="device-state" id="ambientPower">UNKNOWN</span>
</header> </header>
<p class="domain-capability-line"><span>可控参数</span>阅读灯 · 氛围亮度 · 色号</p> <p class="domain-capability-line"><span>可控参数</span>阅读灯 · 氛围亮度 · 色号</p>
<div class="domain-summary-visual ambient-summary-visual" aria-hidden="true"> <div class="domain-summary-visual ambient-summary-visual" aria-hidden="true">
@@ -334,9 +274,9 @@
<span data-ambient-zone="rear"><i id="ambientRearMeter"></i><b>后排</b></span> <span data-ambient-zone="rear"><i id="ambientRearMeter"></i><b>后排</b></span>
</div> </div>
<dl class="domain-readouts"> <dl class="domain-readouts">
<div><dt>氛围灯</dt><dd id="ambientMode">关闭</dd></div> <div><dt>氛围灯</dt><dd id="ambientMode">未知</dd></div>
<div><dt>室内/阅读灯</dt><dd id="interiorLightPower">关闭</dd></div> <div><dt>室内/阅读灯</dt><dd id="interiorLightPower">未知</dd></div>
<div><dt>氛围亮度</dt><dd id="ambientBrightness">0 / 0</dd></div> <div><dt>氛围亮度</dt><dd id="ambientBrightness">- / -</dd></div>
<div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div> <div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div>
</dl> </dl>
</article> </article>
@@ -345,7 +285,7 @@
<header> <header>
<span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span> <span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span>
<div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div> <div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div>
<span class="device-state" id="windowState">READY</span> <span class="device-state" id="windowState">UNKNOWN</span>
</header> </header>
<p class="domain-capability-line"><span>可控参数</span>四窗独立 · 开/关/停 · 位置</p> <p class="domain-capability-line"><span>可控参数</span>四窗独立 · 开/关/停 · 位置</p>
<div class="domain-summary-visual window-summary-visual" aria-hidden="true"> <div class="domain-summary-visual window-summary-visual" aria-hidden="true">
@@ -355,18 +295,18 @@
<span data-window-meter="rear_right"><i></i><b>右后</b></span> <span data-window-meter="rear_right"><i></i><b>右后</b></span>
</div> </div>
<dl class="domain-readouts"> <dl class="domain-readouts">
<div><dt>左前窗</dt><dd id="frontLeftWindow">0%</dd></div> <div><dt>左前窗</dt><dd id="frontLeftWindow">未知</dd></div>
<div><dt>右前窗</dt><dd id="frontRightWindow">0%</dd></div> <div><dt>右前窗</dt><dd id="frontRightWindow">未知</dd></div>
<div><dt>左后窗</dt><dd id="rearLeftWindow">0%</dd></div> <div><dt>左后窗</dt><dd id="rearLeftWindow">未知</dd></div>
<div><dt>右后窗</dt><dd id="rearRightWindow">0%</dd></div> <div><dt>右后窗</dt><dd id="rearRightWindow">未知</dd></div>
</dl> </dl>
</article> </article>
<article class="domain-panel lighting-panel" data-domain-panel="front_light" data-device-channel="ch1"> <article class="domain-panel lighting-panel" data-domain-panel="front_light" data-device-channel="ch1">
<header> <header>
<span class="domain-icon"><i data-lucide="car-front"></i></span> <span class="domain-icon"><i data-lucide="car-front"></i></span>
<div><strong>前大灯</strong><small>FRONT LIGHT · SWITCH</small></div> <div><strong>前大灯</strong><small>FB 0x38A · CH1</small></div>
<span class="device-state" id="frontLightState">OFF</span> <span class="device-state" id="frontLightState">UNKNOWN</span>
</header> </header>
<p class="domain-capability-line"><span>可控参数</span>近/远光 · 雾灯 · 转向灯</p> <p class="domain-capability-line"><span>可控参数</span>近/远光 · 雾灯 · 转向灯</p>
<div class="domain-summary-visual lighting-summary-visual" aria-hidden="true"> <div class="domain-summary-visual lighting-summary-visual" aria-hidden="true">
@@ -376,10 +316,10 @@
<span data-light-meter="turnSignalState"><i></i><b>转向</b></span> <span data-light-meter="turnSignalState"><i></i><b>转向</b></span>
</div> </div>
<dl class="domain-readouts"> <dl class="domain-readouts">
<div><dt>近光灯</dt><dd id="lowBeamState">关闭</dd></div> <div><dt>近光灯</dt><dd id="lowBeamState">未知</dd></div>
<div><dt>远光灯</dt><dd id="highBeamState">关闭</dd></div> <div><dt>远光灯</dt><dd id="highBeamState">未知</dd></div>
<div><dt>雾灯</dt><dd id="fogLightState">关闭</dd></div> <div><dt>雾灯</dt><dd id="fogLightState">未知</dd></div>
<div><dt>转向灯</dt><dd id="turnSignalState">关闭</dd></div> <div><dt>转向灯</dt><dd id="turnSignalState">未知</dd></div>
</dl> </dl>
</article> </article>
@@ -463,11 +403,6 @@
<div class="dynamic-mode-state" data-dynamic-mode> <div class="dynamic-mode-state" data-dynamic-mode>
<i data-lucide="waves"></i><span>动态模式</span><b>未运行</b> <i data-lucide="waves"></i><span>动态模式</span><b>未运行</b>
</div> </div>
<div class="thermal-states">
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
<span data-function="massage"><i data-lucide="waves"></i>按摩 <b>--</b></span>
</div>
<div class="motion-state-grid" aria-live="polite"></div> <div class="motion-state-grid" aria-live="polite"></div>
</article> </article>
<article class="seat-card" data-seat="driver"> <article class="seat-card" data-seat="driver">
@@ -563,7 +498,6 @@
</main> </main>
</div> </div>
<div class="gesture-backdrop" id="gestureBackdrop" hidden></div>
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle"> <aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
<header class="gesture-drawer-header"> <header class="gesture-drawer-header">
<div> <div>
@@ -633,6 +567,51 @@
</section> </section>
</div> </div>
<div class="mode-editor-backdrop" id="modeEditorBackdrop" hidden>
<section class="mode-editor" role="dialog" aria-modal="true" aria-labelledby="modeEditorTitle" aria-describedby="modeEditorDescription">
<div class="mode-editor-setup">
<header class="mode-editor-header">
<div>
<span>MODE WORKFLOW</span>
<h2 id="modeEditorTitle">编辑功能模式</h2>
<p id="modeEditorDescription">执行后生成 CAN 控制指令流;完成后永久保存到当前模式卡片。</p>
</div>
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
</header>
<div class="mode-editor-inputs">
<label class="mode-editor-field mode-editor-name-field" for="modeEditorName">
<span>模式名称</span>
<input id="modeEditorName" maxlength="64" autocomplete="off" placeholder="输入模式名称" />
</label>
<label class="mode-editor-field mode-editor-instruction-field" for="modeEditorInstruction">
<span>自然语言指令</span>
<textarea id="modeEditorInstruction" maxlength="1000" rows="3" placeholder="例如:空调24度、风量2档,打开遮阳帘"></textarea>
</label>
</div>
</div>
<section class="mode-editor-flow" aria-live="polite">
<header>
<span><i data-lucide="workflow"></i>CAN 控制指令流</span>
<div class="mode-editor-flow-tools">
<b id="modeEditorFlowCount">尚未生成</b>
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开 CAN 控制指令区域" title="展开 CAN 控制指令区域">
<i data-lucide="maximize-2"></i>
</button>
</div>
</header>
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
<p>点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。</p>
</div>
</section>
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新执行,才能点击完成永久保存。</p>
<footer class="mode-editor-actions">
<button id="modeEditorCancel" type="button">取消</button>
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="play"></i><span>执行</span></button>
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save"></i><span>完成</span></button>
</footer>
</section>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="toast" id="toast" role="status" aria-live="polite"></div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="color-scheme" content="light" /> <meta name="color-scheme" content="light" />
<title>登录 · Cognitive Agent</title> <title>登录 · Cognitive Agent</title>
<link rel="stylesheet" href="/login.css" /> <link rel="stylesheet" href="/login.css" />
<script src="/network.js?v=android-pad-1" defer></script> <script src="/network.js?v=android-pad-2" defer></script>
<script src="/login.js" defer></script> <script src="/login.js" defer></script>
</head> </head>
<body> <body>
-20
View File
@@ -12,30 +12,11 @@
return { ...state }; return { ...state };
} }
function renderBadge() {
const badge = document.querySelector("#connectionBadge");
if (!badge) return;
const presentation = {
connecting: ["neutral", "loader-circle", "连接中"],
online: ["", "wifi", "服务已连接"],
degraded: ["warning", "wifi-low", "连接不稳定"],
offline: ["danger", "wifi-off", "服务已断开"],
}[state.status];
badge.className = `status-badge connection-badge ${presentation[0]}`.trim();
badge.innerHTML = `<i data-lucide="${presentation[1]}"></i><span>${presentation[2]}</span>`;
badge.title = state.lastError || presentation[2];
window.lucide?.createIcons?.({
attrs: { "aria-hidden": "true" },
nodes: [badge],
});
}
function publish(status, error = null) { function publish(status, error = null) {
const changed = state.status !== status || state.lastError !== error; const changed = state.status !== status || state.lastError !== error;
state.status = status; state.status = status;
state.lastError = error; state.lastError = error;
if (status === "online") state.lastSuccessAt = new Date().toISOString(); if (status === "online") state.lastSuccessAt = new Date().toISOString();
renderBadge();
if (!changed) return; if (!changed) return;
const detail = snapshot(); const detail = snapshot();
listeners.forEach((listener) => listener(detail)); listeners.forEach((listener) => listener(detail));
@@ -110,7 +91,6 @@
function startMonitoring() { function startMonitoring() {
if (probeTimer !== null) return; if (probeTimer !== null) return;
renderBadge();
probe(); probe();
probeTimer = window.setInterval(probe, 10000); probeTimer = window.setInterval(probe, 10000);
window.addEventListener("online", () => { window.addEventListener("online", () => {
+2 -6
View File
@@ -4,12 +4,8 @@
} }
function showUpdate(registration) { function showUpdate(registration) {
const badge = document.querySelector("#pwaBadge"); notify("检测到前端更新,正在刷新页面");
if (!badge) return; registration.waiting?.postMessage({ type: "SKIP_WAITING" });
badge.hidden = false;
badge.innerHTML = '<i data-lucide="refresh-cw"></i><span>发现更新</span>';
badge.onclick = () => registration.waiting?.postMessage({ type: "SKIP_WAITING" });
window.lucide?.createIcons?.({ attrs: { "aria-hidden": "true" }, nodes: [badge] });
} }
async function register() { async function register() {
-2
View File
@@ -14,8 +14,6 @@
display: none; display: none;
} }
.console-v2[data-module="self-extension"] .v2-control-section,
.console-v2[data-module="self-extension"] .v2-config-section,
.console-v2[data-module="self-extension"] .v2-sidebar > .v2-sidebar-footer:not(.v2-extension-footer), .console-v2[data-module="self-extension"] .v2-sidebar > .v2-sidebar-footer:not(.v2-extension-footer),
.console-v2[data-module="self-extension"] .v2-operation-shell, .console-v2[data-module="self-extension"] .v2-operation-shell,
.console-v2[data-module="self-extension"] .v2-output-shell { .console-v2[data-module="self-extension"] .v2-output-shell {
+8 -8
View File
@@ -1,16 +1,16 @@
const CACHE_VERSION = "cognitive-agent-pad-v178"; const CACHE_VERSION = "cognitive-agent-pad-v202";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
"/", "/",
"/index.html", "/index.html",
"/styles.css?v=dual-seat-9", "/styles.css?v=dual-seat-16",
"/network.js?v=android-pad-1", "/network.js?v=android-pad-2",
"/app.js?v=dual-seat-85", "/app.js?v=dual-seat-101",
"/gesture-control.js?v=android-pad-8", "/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-2", "/pwa.js?v=android-pad-3",
"/console-v2.css?v=sync-layout-26", "/console-v2.css?v=sync-layout-40",
"/console-v2.js?v=sync-layout-26", "/console-v2.js?v=sync-layout-41",
"/self-extension.css?v=sync-layout-26", "/self-extension.css?v=sync-layout-26",
"/self-extension.js?v=sync-layout-26", "/self-extension.js?v=sync-layout-26",
"/assets/pets/self-extension-cat.png?v=3", "/assets/pets/self-extension-cat.png?v=3",
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

@@ -0,0 +1,25 @@
# Read-only vehicle-state synchronization through ZLG's Linux VCI SDK.
# Keep the approval token unset: hardware transmit remains locked.
COGNITIVE_AGENT_VEHICLE_CAN_ENABLED=true
COGNITIVE_AGENT_VEHICLE_CAN_BACKEND=zlgcan-linux
COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL=1
COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL=1
COGNITIVE_AGENT_VEHICLE_CAN_ARBITRATION_BITRATE=500000
COGNITIVE_AGENT_VEHICLE_CAN_DATA_BITRATE=2000000
COGNITIVE_AGENT_VEHICLE_CAN_TERMINATION_ENABLED=false
COGNITIVE_AGENT_ZLGCAN_DEVICE_TYPE=33
COGNITIVE_AGENT_ZLGCAN_DEVICE_INDEX=0
COGNITIVE_AGENT_ZLGCAN_LINUX_LIBRARY=/home/orin/project511/project-code/canfd/libusbcanfd.so
COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED=false
# Local NanoMQ wireless dual-CAN relay: CAN0 is the passenger seat; CAN1 is the
# rest of the vehicle. The local relay is executable by default; set the MQTT
# approval flag below to true and configure the shared approval token when a
# deployment needs an additional server-side transmit gate.
COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED=true
COGNITIVE_AGENT_VEHICLE_MQTT_APPROVAL_TOKEN_REQUIRED=false
COGNITIVE_AGENT_VEHICLE_MQTT_HOST=127.0.0.1
COGNITIVE_AGENT_VEHICLE_MQTT_PORT=1883
COGNITIVE_AGENT_VEHICLE_MQTT_CONDITION_TOPIC=n7/n7-headunit/can/state
COGNITIVE_AGENT_VEHICLE_MQTT_CONTROL_TOPIC=n7/n7-headunit/can/command
COGNITIVE_AGENT_VEHICLE_MQTT_QOS=1
+6 -6
View File
@@ -3,7 +3,7 @@ kind: CapabilityCatalog
metadata: metadata:
id: door-native-can-capabilities id: door-native-can-capabilities
name: Door native CAN capabilities name: Door native CAN capabilities
version: 1.2.0 version: 1.3.0
spec: spec:
domain: door domain: door
capabilities: capabilities:
@@ -22,9 +22,9 @@ spec:
risk_level: high risk_level: high
confirmation_policy: always confirmation_policy: always
- name: door.set_rear_lock - name: door.set_rear_lock
description: Observational only and deprecated. Physical control is unsupported on the current CH1; legacy 0x32B is second-row-only and must not be treated as whole-vehicle locking. description: Matrix- and head-unit-backed second-row door lock request on classic CAN ID 0x32B, sub-ID 0x0A. It controls only the rear doors and must never be represented as whole-vehicle locking; physical completion still requires matching 0x407 feedback.
execution_support: observational_only execution_support: matrix_candidate_with_feedback
physical_control_supported: false physical_control_supported: true
target_devices: [rear_door_lock] target_devices: [rear_door_lock]
target_scopes: [rear] target_scopes: [rear]
parameters: parameters:
@@ -32,6 +32,6 @@ spec:
type: string type: string
required: true required: true
allowed_values: [lock, unlock] allowed_values: [lock, unlock]
description: Legacy second-row intent retained only for explicit rejection and compatibility parsing. description: Second-row lock request; lock is raw 1 and unlock is raw 2 at start bit 14.
risk_level: high risk_level: high
confirmation_policy: always confirmation_policy: on_ambiguity
@@ -3,7 +3,7 @@ kind: CapabilityCatalog
metadata: metadata:
id: driver-seat-native-can-capabilities id: driver-seat-native-can-capabilities
name: Driver seat native CAN capabilities name: Driver seat native CAN capabilities
version: 1.0.0 version: 1.1.0
spec: spec:
domain: seat domain: seat
capabilities: capabilities:
@@ -108,3 +108,40 @@ spec:
description: Bounded motion duration; omitted values default to 500 ms. description: Bounded motion duration; omitted values default to 500 ms.
risk_level: high risk_level: high
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
- name: seat.set_driver_position_native
description: >-
Alias for recalling one of the three physical driver/front-left seat memory
slots. The complete 0x4C1/0x15 and /0x16 payloads must have been learned from
the PAD and a fresh 0x494 must prove all four axes initialized before replay.
target_devices: [driver_seat_memory]
target_scopes: [driver, front_left]
parameters:
- name: slot
type: integer
required: true
minimum: 1
maximum: 3
allowed_values: [1, 2, 3]
description: Previously learned PAD memory slot.
risk_level: high
confirmation_policy: on_ambiguity
- name: seat.recall_driver_memory_native
description: >-
Recall one of the three physical driver/front-left seat memory slots by
alternating that slot's learned 0x4C1/0x16 and /0x15 payloads for ten cycles.
0x494 does not expose final absolute motor positions, so delivery is not
represented as proof that the seat reached its final position.
target_devices: [driver_seat_memory]
target_scopes: [driver, front_left]
parameters:
- name: slot
type: integer
required: true
minimum: 1
maximum: 3
allowed_values: [1, 2, 3]
description: Previously learned PAD memory slot.
risk_level: high
confirmation_policy: on_ambiguity
+205
View File
@@ -0,0 +1,205 @@
# Linux ZLG 车辆状态接入
当前工程可以通过 ZLG Linux VCI SDK 被动读取 USBCANFD-200U 的 CH1,并把
车辆状态送入 `/api/hardware/vehicle-state` 和前端整车孪生。状态轮询使用独立
`monitor-only` 桥,桥进程会拒绝所有发送请求。服务启动后由一个常驻监听线程
持续缓存最新的已验证状态,HTTP 接口不会为每次前端请求反复开关 USB 设备。
Linux 只读状态模式的前端刷新周期为 500 ms;缓存只保留 3 秒内的新鲜反馈,
断线后会自动关闭旧桥并重试连接。监听通道只接受本文列出的 12 个标准帧 ID,
同时在 SDK 桥内保留同样的软件过滤,避免把无关总线流量转发给服务进程。
## 硬件和总线基线
- USB:`3068:0009 ZLG USBCANFD-200U`
- Linux SDK 设备类型:`33`
- 通道:CH1
- 帧格式:11-bit ISO CAN FD,按报文保留 BRS 属性
- 控制器时钟:60 MHz
- 仲裁段:500 kbit/s,`tseg1=14, tseg2=3, sjw=2, brp=5`
- 数据段:2 Mbit/s,`tseg1=10, tseg2=2, sjw=2, brp=1`
- 终端:默认不启用适配器内部 120 欧;只能按实际拓扑调整
前端车辆概况依赖 `0x342`(档位)、`0x4EB/0x07`(车速)和 `0x407`
(门锁)。主驾座椅依赖 `0x4FA`、`0x494`。收到其他帧不能替代这些已验证
反馈。车身设备还会按帧独立更新:`0x388` 四窗、`0x404` 空调、`0x41A`
遮阳帘、`0x42E/0x3F3/0x3FF` 车内灯光,以及 `0x38A` 前部灯光。
## 前端有线 / 无线选择
前端“车辆连接方式”提供两个相互独立的传输选项:
- 有线控制:状态和控制都走后端 ZLG CH1。
- 无线控制:状态订阅 `n7/n7-headunit/can/state`,控制发布到
`n7/n7-headunit/can/command`;默认 Broker 为 `127.0.0.1:1883`。MQTT CAN0 专用于
副驾座椅,MQTT CAN1 专用于除副驾座椅外的车辆状态和控制。
选择保存在当前浏览器,并随车辆状态请求和控制请求一起传给后端。切换传输时
会建立新的会话上下文,上一条链路缓存的状态不会冒充当前链路状态。两种方式
共用同一套编译范围检查、P 挡/零车速预检、反馈目标匹配和硬件批准令牌;MQTT
发布成功只证明 Broker 接收了消息,不能替代 ECU 状态反馈。
MQTT 状态主题只承载原始 CAN 帧,不接受已经解析的车辆状态 JSON。接收侧继续
兼容下面的规范单帧格式:
```json
{
"schema": "n7.can.frame.v1",
"arbitration_id": 834,
"frame_id": "0x342",
"channel": 1,
"data_hex": "00 00 00 01 00 00 00 00",
"data": [0, 0, 0, 1, 0, 0, 0, 0],
"payload_size": 8,
"is_fd": true,
"bitrate_switch": true,
"timestamp": 1787029200.0
}
```
车机实际使用的 `n7-can-signals/v1` 状态包由顶层 `signals` 承载同样的原始帧。
`n7/n7-headunit/can/state` 可以发送单帧对象,也可以发送
`{"frames":[...]}` 批次。接收侧兼容 `can_id`、`canId`、`frame_id` 和
`dataHex` 等常见字段名,但仍强制 CAN0/CAN1、11 位 ID、1–64 字节长度和
逐通道反馈 ID 白名单。
自然语言指令仍先由后端编译、检查白名单和车辆安全条件。每个已批准的 CAN 帧
再转换为车机 App `CanCommand` 格式并发布到
`n7/n7-headunit/can/command`:
```json
{
"requestId": "cognitive-agent-0e77879b83fd47a9bf12efb3d20dfae7",
"channel": "can1",
"canId": "0x431",
"frameType": "fd",
"brs": true,
"extended": false,
"remote": false,
"data": "10 00 00 00 00 00 00 00",
"repeat": 1,
"intervalMs": 0,
"responseTimeoutMs": 1500
}
```
每次发布生成不同的 `requestId`。MQTT 外层固定 `repeat=1`,原有执行器继续负责
矩阵要求的重复次数、周期和动作后的安全停止,不会在车机端再次叠加重复发送。
CAN0 输出 `classic` 且禁用 BRS;CAN1 按编译结果输出 Classic CAN 或合法长度的
FD+BRS。车机按通道、控制 ID 和帧类型推断预期反馈 ID。
无线通道边界为:
- CAN0:经典 CAN 副驾座椅;直接状态接收 `0x301–0x307`、`0x4FA`。当前车端
还会以只读 `0x6A0` `SEAT_INFO(0x60)` 查询和 `0x6A2` 响应上报四轴 Hall
位置、范围和标志。后端只配对 `C7/7C`、相同序号、`status=0` 的查询响应,
再转换成统一的四轴 `0x302` 状态;查询帧不会被当作普通动作执行。
无线副驾控制只允许 `0x6A0` 中固件明确提供的运行时 opcode:
`STOP(0x63)`、`MOVE(0x64)`、`HEARTBEAT(0x65)` 和
`DYNAMIC_START(0x66)`,并等待匹配序号/opcode 的 `0x6A2` 成功响应及新鲜
四轴位置反馈。刷写、进入 Bootloader、测试、校准和回零 opcode 全部拒绝;
旧 `0x29E/0x300/0x3CE` 不会在该无线 CAN0 路径发布。
- CAN1:除副驾座椅以外的整车 CAN;状态和控制继续使用项目中已有的 CH1
车辆白名单。CAN0 控制 ID 不能经 CAN1 发送,CAN1 控制 ID 也不能经 CAN0
发送。
两个通道的协议来源严格分离:
- CAN0 只以 `/home/orin/project511/seat-code-file/seat-ota` 副驾固件为准;
入站状态必须是 11 位标准帧、经典 CAN、无 BRS、DLC 8。
- CAN1 只以
`/home/orin/project511/project-code/canfd/CAN矩阵表_filled.xlsx` 为准。
例如 `0x42E` 颜色值只有 `0x01–0x1F` 有效,保留值不会显示为
真实颜色。`0x388` 的四窗关好位按矩阵解析;byte 3–6 的四窗百分比另有
`project-code/canfd/window_native.txt` 实车变化轨迹和车端原始帧证据,
因此只有数值落在 `0–100` 时才显示为开度。
MQTT 消息自带的 `can0Seat.parsed` 和 `can1Vehicle.parsed` 只可用于车端
诊断,后端不使用它们更新状态或通过安全门禁。
副驾位置换算严格使用固件 `OTA_SEAT_INFO_AXIS0..3_POSITION_RANGE`:响应值低
16 位为当前位置 Hall 计数,高 16 位为该轴 Hall 范围;
`OTA_SEAT_INFO_AXIS0..3_FLAGS` 提供 `valid/moving/driver_fault/direction`。四轴
分别映射为水平、坐垫、靠背和腿托。只读缓存最多保留 3 秒,新建状态请求可
立即复用新鲜四轴快照,不需要等待下一轮车端查询。
每个 MQTT 总线视图只会收到本通道反馈。发布完成仅代表 Broker 接收消息;
座椅和整车执行器仍必须等待对应状态 ID 与目标字段匹配,才会报告动作成功。
当前 seat-ota 固件只提供四轴位置和动态模式控制,没有副驾加热/通风运行时
opcode;无线模式收到这两类副驾请求会在发布前明确失败,不会伪造反馈。
车端现有 `n7-can-signals/v1` 状态封装也可直接发布到
`n7/n7-headunit/can/state`。后端优先从顶层 `signals` 读取,避免与嵌套副本重复;
顶层缺失时会合并 `can0Seat.signals`(默认 CAN0)和
`can1Vehicle.signals`(默认 CAN1)。接收兼容 `channel: "can0"/"can1"`、
`channelIndex`、`frameType`、`brs`、`data`/`bytes` 和毫秒时间戳。若同一帧的
`channel` 与 `channelIndex` 冲突,整批消息会被拒绝。封装中的
`can0Seat.parsed` 和 `can1Vehicle.parsed` 都不作为权威状态输入;安全预检和
前端状态仍由原始 CAN 帧通过逐通道白名单解码器生成。带 Unix 时间戳的过期
CAN1 非安全帧可短暂作为 `display_only` 缓存显示,但不会进入控制执行器的
反馈队列。过期的 `0x342` 挡位和 `0x4EB` 车速帧会直接拒绝,绝不能用
旧的 P 挡或零车速结果通过当前控制预检。
无线 CAN0 和 CAN1 状态读取互不持有真实执行独占锁。CAN0 状态缺失或等待时
不会再让 CAN1 接口返回 `hardware_busy`。前端按通道分别保持 single-flight,
CAN1 以 500 ms 周期刷新;手动切换有线/无线后会立即按所选后端重新建立轮询
周期,上一传输方式的迟到响应会被丢弃。
无线副驾动作在向 CAN0 发布前,也必须通过同一条 MQTT 链路上的 CAN1 新鲜
P 挡与零车速预检。远端链路不会触发本机 IAR 自动烧录;副驾固件升级仍需走
独立、明确授权的维护流程。
## USB 权限
设备节点必须允许运行服务的用户读写。推荐使用仅授予当前桌面登录用户权限的
udev 规则:
```bash
sudo sh -c 'printf "%s\n" '\''SUBSYSTEM=="usb", ATTR{idVendor}=="3068", ATTR{idProduct}=="0009", TAG+="uaccess", MODE="0660"'\'' > /etc/udev/rules.d/50-zlg-usbcanfd.rules'
sudo udevadm control --reload-rules
sudo udevadm trigger --attr-match=idVendor=3068 --attr-match=idProduct=0009
```
若 ACL 没有立即出现,重新插拔适配器。使用以下命令核对,当前服务用户应具有
`rw`:
```bash
getfacl -p /dev/bus/usb/001/006
```
设备编号每次插拔可能变化,应以 `lsusb` 的 Bus/Device 为准。
## 服务配置
将 `config/environments/linux_vehicle_state.example.env` 中的配置复制到仓库
根目录 `.env`,并按实际 SDK 位置修改
`COGNITIVE_AGENT_ZLGCAN_LINUX_LIBRARY`。不要设置硬件批准令牌;此时
`vehicle_can.state_configured=true`,但 `vehicle_can.configured=false`,所以只
允许状态同步,前端真实执行保持锁定。
启动:
```bash
make dev
```
核对运行时:
```bash
curl -s http://127.0.0.1:8000/api/runtime
```
车辆状态接口在有线模式应返回 `source=physical_can_ch1`,在无线模式应返回
`source=wireless_mqtt_ch1`,并包含实际收到的 `observed_frame_ids` 和缺失反馈
列表。前端“整车设备状态”会把这两个来源都作为权威实时状态,并明确显示
“有线 CAN”或“无线 CAN”。发送成功、CAN ACK 和状态反馈是不同证据层级;
只有接口实际收到并解码目标反馈才会更新前端。
## SDK 自检
自检只加载动态库和核对 ABI,不打开 USB:
```bash
.venv/bin/python src/cognitive_agent/infrastructure/can/zlgcan_linux_bridge.py \
--dll /home/orin/project511/project-code/canfd/libusbcanfd.so \
--self-test
```
@@ -121,6 +121,26 @@
} }
} }
}, },
{
"type": "object",
"properties": {
"capability": {
"type": "string",
"enum": [
"seat.set_driver_position_native",
"seat.recall_driver_memory_native"
]
},
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["slot"],
"properties": {
"slot": {"type": "integer", "minimum": 1, "maximum": 3}
}
}
}
},
{ {
"type": "object", "type": "object",
"properties": { "properties": {

Some files were not shown because too many files have changed in this diff Show More