Compare commits
3 Commits
772ab685d6
...
8d05815773
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d05815773 | |||
| fff7657fea | |||
| 52c4dd08f9 |
@@ -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/
|
||||||
|
|||||||
@@ -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`。
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 466 KiB |
|
Before Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||