diff --git a/README.md b/README.md
index b405840..06b6b75 100755
--- a/README.md
+++ b/README.md
@@ -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 模式只应在可信网络内临时演示。
+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`。
diff --git a/apps/web-console/app.js b/apps/web-console/app.js
index 7e3ce7d..ea31ef8 100755
--- a/apps/web-console/app.js
+++ b/apps/web-console/app.js
@@ -39,10 +39,16 @@ function loadHardwareClientId() {
return clientId;
}
+function loadControlTransport() {
+ const stored = localStorage.getItem("cognitive-agent.control-transport");
+ return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
+}
+
const state = {
provider: "openai",
reasoningEffort: localStorage.getItem("cognitive-agent.reasoning-effort") || "medium",
executionMode: "dry_run",
+ controlTransport: loadControlTransport(),
conversationId: loadConversationId(),
vehicleId: null,
runtime: null,
@@ -61,6 +67,7 @@ const state = {
hardwareAccessPoller: null,
physicalStatePoller: null,
physicalStateSyncPromise: null,
+ passengerStateSyncPromise: null,
pendingModeProfileId: null,
modeRunTimer: null,
modeDraftCommand: "",
@@ -76,11 +83,18 @@ const apiRequest = (input, options) => window.CognitiveAgentNetwork.request(inpu
// workflow so the UI does not report failure while the seat is still moving.
const HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 45 * 1000;
+const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
function requestTimeoutForExecutionMode(mode) {
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
}
+function physicalStatePollIntervalMs() {
+ const configured = Number(selectedVehicleTransport()?.state_sync?.poll_interval_ms);
+ if (!Number.isFinite(configured)) return DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS;
+ return Math.max(250, Math.min(DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS, configured));
+}
+
function refreshIcons() {
if (window.lucide) {
window.lucide.createIcons({ attrs: { "aria-hidden": "true" } });
@@ -4222,12 +4236,80 @@ function isLocalDecisionHost() {
return ["127.0.0.1", "localhost", "::1"].includes(window.location.hostname);
}
+function vehicleTransportOptions() {
+ return Array.isArray(state.runtime?.vehicle_transports?.options)
+ ? state.runtime.vehicle_transports.options
+ : [];
+}
+
+function selectedVehicleTransport() {
+ return vehicleTransportOptions().find((item) => item.id === state.controlTransport)
+ || (state.controlTransport === "wireless_mqtt"
+ ? state.runtime?.vehicle_mqtt
+ : state.runtime?.vehicle_can)
+ || null;
+}
+
function isDriverOnlyCh1Runtime() {
- return state.runtime?.vehicle_can?.configured === true
- && state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"
+ const transport = selectedVehicleTransport();
+ return transport?.configured === true
+ && transport?.target_policy === "ch1_body_can_driver_only"
&& state.runtime?.hardware?.configured !== true;
}
+function renderControlTransport() {
+ const options = vehicleTransportOptions();
+ $$('[data-control-transport]').forEach((button) => {
+ const id = button.dataset.controlTransport;
+ const runtime = options.find((item) => item.id === id)
+ || (id === "wireless_mqtt" ? state.runtime?.vehicle_mqtt : state.runtime?.vehicle_can);
+ const selected = id === state.controlTransport;
+ button.classList.toggle("active", selected);
+ button.setAttribute("aria-pressed", String(selected));
+ // Transport choice is intentionally manual. An unavailable connection is
+ // shown as offline, but it must never disable or silently change the
+ // operator's wired/wireless selection.
+ button.disabled = false;
+ button.dataset.connectionState = runtime?.state_sync?.state === "online" ? "online" : "offline";
+ button.title = runtime?.state_configured === true
+ ? runtime?.state_sync?.last_error || ""
+ : `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
+ });
+ const selected = selectedVehicleTransport();
+ const note = $("#vehicleTransportNote");
+ if (!note) return;
+ if (selected?.state_configured !== true) {
+ note.textContent = state.controlTransport === "wireless_mqtt"
+ ? "已手动选择无线控制 · MQTT 当前未配置或不可用"
+ : "已手动选择有线控制 · ZLG CAN 当前未配置或不可用";
+ return;
+ }
+ if (state.controlTransport === "wireless_mqtt") {
+ const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
+ note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/can_control"}`;
+ } else {
+ const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
+ note.textContent = `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
+ }
+}
+
+function selectControlTransport(transport) {
+ if (!["wired_can", "wireless_mqtt"].includes(transport)) return;
+ const changed = state.controlTransport !== transport;
+ state.controlTransport = transport;
+ localStorage.setItem("cognitive-agent.control-transport", transport);
+ if (changed) {
+ state.conversationId = createConversationId();
+ sessionStorage.setItem("cognitive-agent.conversation-id", state.conversationId);
+ state.seatFeedbackSources.passenger = "unavailable";
+ state.seatFeedbackSources.driver = "unavailable";
+ }
+ state.vehicleStateSnapshot = null;
+ state.vehicleStateTurn = -1;
+ renderControlTransport();
+ applyRuntimeTargetPolicy(state.runtime);
+}
+
function renderHardwareAccess(status) {
state.hardwareAccess = status;
}
@@ -4258,6 +4340,8 @@ function updateSeatFeedbackModeLabel() {
physical_can: "实车已同步",
physical_partial: "物理反馈不完整",
physical_can_ch1: "实车已同步",
+ wireless_mqtt_ch0: "MQTT 已同步",
+ wireless_mqtt_ch1: "MQTT 已同步",
simulated_hardware: "模拟 HIL",
simulated: "虚拟反馈",
unavailable: "不可用",
@@ -4266,6 +4350,8 @@ function updateSeatFeedbackModeLabel() {
physical_can: "实车",
physical_partial: "部分",
physical_can_ch1: "实车",
+ wireless_mqtt_ch0: "MQTT",
+ wireless_mqtt_ch1: "MQTT",
simulated_hardware: "模拟",
simulated: "虚拟",
unavailable: "未连接",
@@ -4283,7 +4369,11 @@ function updateSeatFeedbackModeLabel() {
}
function selectExecutionMode(mode) {
- if (mode === "hardware" && !state.runtime?.hardware_execution_allowed) return;
+ const selectedTransport = selectedVehicleTransport();
+ const hardwareExecutionReady = Boolean(
+ state.runtime?.hardware?.configured || selectedTransport?.configured,
+ );
+ if (mode === "hardware" && !hardwareExecutionReady) return;
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return;
state.executionMode = mode;
$$('[data-execution-mode]').forEach((button) => {
@@ -4291,7 +4381,8 @@ function selectExecutionMode(mode) {
});
const hardware = mode === "hardware";
const simulatedHardware = mode === "simulated_hardware";
- const vehicleCanReady = Boolean(state.runtime?.vehicle_can?.configured);
+ const vehicleCanReady = Boolean(selectedTransport?.configured);
+ const mqttSelected = state.controlTransport === "wireless_mqtt";
const ch1DriverOnly = isDriverOnlyCh1Runtime();
$("#simulationFaultField").hidden = !simulatedHardware;
$("#executionBadge").className = `status-badge ${hardware ? "" : simulatedHardware ? "warning" : "neutral"}`.trim();
@@ -4305,7 +4396,7 @@ function selectExecutionMode(mode) {
: simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界";
$("#safetyText").textContent = hardware
? ch1DriverOnly && vehicleCanReady
- ? "仅使用 ZLG CH1 控制主驾座椅与已验证车身设备;每次动作前必须确认 P 挡和 0 km/h,并以 CH1 状态帧回读为准。CH0、副驾 T2G 与 IAR 路径均不参与。"
+ ? `仅使用${mqttSelected ? "本地 MQTT 无线链路" : "ZLG 有线 CH1"}控制主驾座椅与已验证车身设备;每次动作前必须确认 P 挡和 0 km/h,并以 CH1 状态帧回读为准。CH0、副驾 T2G 与 IAR 路径均不参与。`
: vehicleCanReady
? "同一 CAN 盒由单进程持有:CH0 控制副驾四电机并校验板端反馈;CH1 控制主驾四电机与车身设备,每次动作前校验 P 挡、零车速和对应状态帧。"
: "当前仅通道 0 连接副驾座椅台架并校验板端反馈;车端 CAN 未启用,模式中的其他车舱领域保持虚拟执行。"
@@ -4315,7 +4406,9 @@ function selectExecutionMode(mode) {
$("#executeStageLabel").textContent = hardware ? "台架执行" : simulatedHardware ? "故障仿真" : "虚拟执行";
$("#feedbackModeLabel").textContent = hardware
? (ch1DriverOnly
- ? "CH1 BODY CAN · AUTHORITATIVE PHYSICAL FEEDBACK"
+ ? (mqttSelected
+ ? "MQTT CH1 · AUTHORITATIVE CAN FEEDBACK"
+ : "CH1 BODY CAN · AUTHORITATIVE PHYSICAL FEEDBACK")
: "T2G BENCH · PHYSICAL CAN FEEDBACK")
: simulatedHardware
? "SIMULATED HIL · FAULT INJECTION"
@@ -5019,13 +5112,18 @@ async function saveCurrentModeProfile() {
}
function applyRuntimeTargetPolicy(runtime) {
- const passengerReady = runtime?.hardware?.configured === true;
- const driverReady = runtime?.vehicle_can?.configured === true;
+ const selectedTransport = selectedVehicleTransport();
+ const wirelessDualChannel = state.controlTransport === "wireless_mqtt"
+ && selectedTransport?.target_policy === "dual_channel_mqtt_can0_passenger_can1_vehicle";
+ const passengerReady = wirelessDualChannel
+ ? selectedTransport?.configured === true
+ : runtime?.hardware?.configured === true;
+ const driverReady = selectedTransport?.configured === true;
const driverOnly = driverReady && !passengerReady;
const stage = $("#seatTwin");
if (stage) {
stage.dataset.passengerChannel = "CH0";
- stage.dataset.driverChannel = "CH1";
+ stage.dataset.driverChannel = state.controlTransport === "wireless_mqtt" ? "MQTT" : "CH1";
stage.dataset.passengerConfigured = String(passengerReady);
stage.dataset.driverConfigured = String(driverReady);
}
@@ -5072,6 +5170,7 @@ async function loadRuntime() {
const response = await apiRequest("/api/runtime", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
state.runtime = await response.json();
+ renderControlTransport();
applyRuntimeTargetPolicy(state.runtime);
populateVehicleSelector();
populateModeChips();
@@ -5080,12 +5179,17 @@ async function loadRuntime() {
state.reasoningEffort = configuredEffort;
}
const hardware = state.runtime.hardware || {};
- const vehicleCan = state.runtime.vehicle_can || {};
- const linkedHardwareReady = Boolean(hardware.configured && vehicleCan.configured);
- const seatHardwareReady = Boolean(hardware.configured);
+ const vehicleCan = selectedVehicleTransport() || {};
+ const mqttSelected = state.controlTransport === "wireless_mqtt";
+ const mqttDualChannel = mqttSelected
+ && vehicleCan.target_policy === "dual_channel_mqtt_can0_passenger_can1_vehicle";
+ const seatHardwareReady = Boolean(
+ mqttDualChannel ? vehicleCan.configured : hardware.configured,
+ );
const vehicleHardwareReady = Boolean(vehicleCan.configured);
+ const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;
const ch1DriverOnly = vehicleHardwareReady && !seatHardwareReady;
- const hardwareExecutionReady = Boolean(state.runtime.hardware_execution_allowed);
+ const hardwareExecutionReady = Boolean(hardware.configured || vehicleCan.configured);
const badge = $("#modelBadge");
const gptButton = $('[data-provider="openai"]');
if (state.runtime.openai_available) {
@@ -5120,32 +5224,35 @@ async function loadRuntime() {
&& hardware.access_policy === "local_approval_idle_lease"
? ` · 本地审批 · 空闲 ${hardware.access_inactivity_timeout_seconds || 180} 秒自动停止`
: "";
+ const vehicleRouteText = mqttSelected
+ ? `MQTT ${vehicleCan.host || "127.0.0.1"}:${vehicleCan.port || 1883} · ${vehicleCan.condition_topic || "n7/n7-headunit/can/state"} / ${vehicleCan.control_topic || "n7/can_control"}`
+ : `${vehicleCan.backend || "zlgcan"} · CH${vehicleCan.channel ?? 1} 车身 CAN FD ${Number(vehicleCan.arbitration?.bitrate || 500000) / 1000}k/${Number(vehicleCan.data?.bitrate || 2000000) / 1000000}M`;
$("#hardwareNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
- ? `${vehicleCan.backend} · CH${vehicleCan.channel} 车身 CAN FD ${vehicleCan.arbitration.bitrate / 1000}k/${vehicleCan.data.bitrate / 1000000}M · 仅主驾/车端白名单 · CH0/T2G/IAR 已禁用${accessText}${concurrencyText}`
+ ? `${vehicleRouteText} · 仅主驾/车端白名单 · CH0/T2G/IAR 已禁用${accessText}${concurrencyText}`
: linkedHardwareReady
- ? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · CH${vehicleCan.channel} 车身 CAN FD ${vehicleCan.arbitration.bitrate / 1000}k/${vehicleCan.data.bitrate / 1000000}M · ${profileText} · ${flashText}${accessText}${concurrencyText}`
+ ? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText} · ${profileText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${flashText}`}${accessText}${concurrencyText}`
: seatHardwareReady
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
- : `联动真实执行已锁定:${profileText};请完成座椅固件、双通道 ZLG 适配器和车身 CAN FD 配置`;
+ : `真实执行已锁定:${profileText};当前${mqttSelected ? "无线 MQTT" : "有线 CAN"}控制未通过后端审批配置`;
$("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
- ? "CH1 → 车身 CAN FD / 主驾座椅 · CH0 与副驾 T2G 路径禁止"
+ ? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
: linkedHardwareReady
- ? "CH0 → 副驾座椅控制器 · CH1 → 车身 CAN FD"
+ ? `${mqttDualChannel ? "MQTT CAN0" : "CH0"} → 副驾座椅控制器 · ${mqttDualChannel ? "MQTT CAN1" : "CH1"} → 其余车辆 CAN`
: seatHardwareReady
? `CH${hardware.channel} → 副驾座椅控制器 · 车身控制暂用虚拟 CAN`
: "尚未配置可执行的物理 CAN 通道";
$("#hardwareBadge").className = `status-badge ${hardwareExecutionReady ? "" : "danger"}`.trim();
$("#hardwareBadge").innerHTML = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
- ? 'CH1 车端已配置'
+ ? `${mqttSelected ? "MQTT" : "CH1"} 车端已配置`
: linkedHardwareReady
? '双通道已配置'
: seatHardwareReady
? '座椅台架已配置'
: '硬件锁定';
$("#hardwareCoverage").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
- ? `CH1 主驾 + 车端设备 · 物理字段回读${accessText}${concurrencyText}`
+ ? `${mqttSelected ? "MQTT" : "CH1"} 主驾 + 车端设备 · CAN 字段回读${accessText}${concurrencyText}`
: linkedHardwareReady
- ? `CH0 座椅 + CH${vehicleCan.channel} 车身${accessText}${concurrencyText}`
+ ? `CH0 座椅 + ${mqttSelected ? "MQTT" : `CH${vehicleCan.channel}`} 车身${accessText}${concurrencyText}`
: seatHardwareReady
? `CH${hardware.channel} 座椅 · 物理反馈${accessText}${concurrencyText}`
: "已阻断";
@@ -5166,10 +5273,16 @@ async function loadRuntime() {
}
async function synchronizePassengerSeatState() {
- if (!state.runtime?.hardware?.configured) return false;
+ const requestedTransport = state.controlTransport;
+ const mqttSelected = requestedTransport === "wireless_mqtt";
+ const stateReady = mqttSelected
+ ? selectedVehicleTransport()?.state_configured
+ : state.runtime?.hardware?.configured;
+ if (!stateReady) return false;
const query = new URLSearchParams({
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
+ control_transport: requestedTransport,
hardware_client_id: state.hardwareClientId,
});
try {
@@ -5182,15 +5295,16 @@ async function synchronizePassengerSeatState() {
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
+ if (requestedTransport !== state.controlTransport) return false;
renderSeatState({ passenger: payload.actuator_state?.passenger || {} });
state.seatFeedbackSources.passenger = payload.complete
- ? "physical_can"
+ ? payload.source === "wireless_mqtt_ch0" ? "wireless_mqtt_ch0" : "physical_can"
: "physical_partial";
state.seatFeedbackSource = state.seatFeedbackSources.passenger;
updateSeatFeedbackModeLabel();
return true;
} catch (_error) {
- if (!state.seatFeedbackSources.passenger.startsWith("physical")) {
+ if (!["physical_can", "wireless_mqtt_ch0"].includes(state.seatFeedbackSources.passenger)) {
state.seatFeedbackSources.passenger = "unavailable";
updateSeatFeedbackModeLabel();
}
@@ -5199,10 +5313,12 @@ async function synchronizePassengerSeatState() {
}
async function synchronizeDriverSeatState() {
- if (!state.runtime?.vehicle_can?.configured) return false;
+ if (!selectedVehicleTransport()?.state_configured) return false;
+ const requestedTransport = state.controlTransport;
const query = new URLSearchParams({
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
+ control_transport: requestedTransport,
hardware_client_id: state.hardwareClientId,
});
try {
@@ -5213,13 +5329,16 @@ async function synchronizeDriverSeatState() {
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
+ if (requestedTransport !== state.controlTransport) return false;
applyAuthoritativeVehicleState(payload);
- state.seatFeedbackSources.driver = payload.complete ? "physical_can_ch1" : "physical_partial";
+ state.seatFeedbackSources.driver = payload.complete
+ ? payload.source === "wireless_mqtt_ch1" ? "wireless_mqtt_ch1" : "physical_can_ch1"
+ : "physical_partial";
state.seatFeedbackSource = state.seatFeedbackSources.driver;
updateSeatFeedbackModeLabel();
return true;
} catch (_error) {
- if (!state.seatFeedbackSources.driver.startsWith("physical")) {
+ if (!["physical_can_ch1", "wireless_mqtt_ch1"].includes(state.seatFeedbackSources.driver)) {
state.seatFeedbackSources.driver = "unavailable";
updateSeatFeedbackModeLabel();
}
@@ -5230,7 +5349,22 @@ async function synchronizeDriverSeatState() {
async function synchronizePhysicalSeatState() {
if (state.physicalStateSyncPromise) return state.physicalStateSyncPromise;
state.physicalStateSyncPromise = (async () => {
- // Both channels share one adapter owner; keep passive reads serialized.
+ if (state.controlTransport === "wireless_mqtt") {
+ // CAN0 may legitimately be absent and wait for its bounded seat-status
+ // timeout. Keep that wait single-flight in the background so cached
+ // CAN1 vehicle state can still refresh at the MQTT polling cadence.
+ if (!state.passengerStateSyncPromise) {
+ let passengerPromise;
+ passengerPromise = synchronizePassengerSeatState().finally(() => {
+ if (state.passengerStateSyncPromise === passengerPromise) {
+ state.passengerStateSyncPromise = null;
+ }
+ });
+ state.passengerStateSyncPromise = passengerPromise;
+ }
+ return synchronizeDriverSeatState();
+ }
+ // The wired channels share one adapter owner; keep passive reads serialized.
const passenger = await synchronizePassengerSeatState();
const driver = await synchronizeDriverSeatState();
return passenger || driver;
@@ -5242,6 +5376,16 @@ async function synchronizePhysicalSeatState() {
}
}
+function restartPhysicalStatePolling() {
+ if (state.physicalStatePoller) window.clearInterval(state.physicalStatePoller);
+ state.physicalStatePoller = window.setInterval(() => {
+ const activeModule = document.body.dataset.module;
+ if (document.visibilityState !== "visible") return;
+ if (!["self-orchestration", "self-extension"].includes(activeModule)) return;
+ synchronizePhysicalSeatState();
+ }, physicalStatePollIntervalMs());
+}
+
async function loadAuthStatus() {
const response = await apiRequest("/api/auth/status", { cache: "no-store" });
if (!response.ok) return;
@@ -5542,7 +5686,7 @@ function renderDomainState(domainState) {
if (vehicleSystems) {
vehicleSystems.targets.climateLevel = Math.max(0, Math.min(7, Number(climate.fan_level) || 0));
vehicleSystems.targets.climateTemperature = Number(
- state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"
+ selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only"
? driverTemperature
: passengerTemperature,
) || 24;
@@ -6170,8 +6314,8 @@ function renderExecution(result) {
: simulatedHil
? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN`
: physical
- ? state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only"
- ? `${executedDomains.join(" / ")} 已通过 CH1 下发,并以车端状态字段确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
+ ? selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only"
+ ? `${executedDomains.join(" / ")} 已通过${result.control_transport === "wireless_mqtt" ? " MQTT" : " CH1"}下发,并以车端状态字段确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
if (partiallySucceeded) {
@@ -6306,8 +6450,12 @@ function applyAuthoritativeVehicleState(payload) {
domainState: domainPatch,
};
if (actuatorPatch?.driver) {
- state.seatFeedbackSource = "physical_can_ch1";
- state.seatFeedbackSources.driver = "physical_can_ch1";
+ const source = payload.control_transport === "wireless_mqtt"
+ || payload.source === "wireless_mqtt_ch1"
+ ? "wireless_mqtt_ch1"
+ : "physical_can_ch1";
+ state.seatFeedbackSource = source;
+ state.seatFeedbackSources.driver = source;
}
renderSeatState(actuatorPatch);
renderDomainState(domainPatch);
@@ -6346,8 +6494,10 @@ function renderResult(result) {
if (result.actuator_state?.driver) state.seatFeedbackSources.driver = "simulated_hardware";
} else if (feedbackSource === "physical_can_ch1"
&& isAuthoritativeVehicleStatePayload(result)) {
- state.seatFeedbackSource = "physical_can_ch1";
- state.seatFeedbackSources.driver = "physical_can_ch1";
+ state.seatFeedbackSource = result.control_transport === "wireless_mqtt"
+ ? "wireless_mqtt_ch1"
+ : "physical_can_ch1";
+ state.seatFeedbackSources.driver = state.seatFeedbackSource;
} else if (reportMode === "hil") {
state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete
? "physical_can"
@@ -6393,7 +6543,7 @@ function renderResult(result) {
renderTrace(result.trace_events || []);
renderValidation(result.validation || []);
const ch1HardwareResult = reportMode === "hil"
- && state.runtime?.vehicle_can?.target_policy === "ch1_body_can_driver_only";
+ && selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only";
if (ch1HardwareResult) {
applyAuthoritativeVehicleState(result);
} else {
@@ -6457,6 +6607,7 @@ async function submitRequest(event) {
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
execution_mode: state.executionMode,
+ control_transport: state.controlTransport,
...(state.provider === "openai" ? { reasoning_effort: state.reasoningEffort } : {}),
...(state.executionMode === "hardware" && !driverOnlyCh1
? { hardware_client_id: state.hardwareClientId }
@@ -6716,6 +6867,12 @@ document.addEventListener("DOMContentLoaded", async () => {
$$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider)));
$$('[data-reasoning-effort]').forEach((button) => button.addEventListener("click", () => selectReasoningEffort(button.dataset.reasoningEffort)));
$$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode)));
+ $$('[data-control-transport]').forEach((button) => button.addEventListener("click", async () => {
+ selectControlTransport(button.dataset.controlTransport);
+ await loadRuntime();
+ restartPhysicalStatePolling();
+ await synchronizePhysicalSeatState();
+ }));
$$(".example-command").forEach((button) => button.addEventListener("click", () => {
$("#utterance").value = button.dataset.example;
}));
@@ -6732,12 +6889,7 @@ document.addEventListener("DOMContentLoaded", async () => {
);
}
await synchronizePhysicalSeatState();
- state.physicalStatePoller = window.setInterval(() => {
- const activeModule = document.body.dataset.module;
- if (document.visibilityState !== "visible") return;
- if (!["self-orchestration", "self-extension"].includes(activeModule)) return;
- synchronizePhysicalSeatState();
- }, 5000);
+ restartPhysicalStatePolling();
if (new URLSearchParams(window.location.search).get("autorun") === "1") {
$("#requestForm").requestSubmit();
}
diff --git a/apps/web-console/console-v2.css b/apps/web-console/console-v2.css
index caad520..aa56e4d 100644
--- a/apps/web-console/console-v2.css
+++ b/apps/web-console/console-v2.css
@@ -346,6 +346,63 @@
display: none;
}
+.console-v2 .v2-transport-field {
+ padding: 8px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(255, 255, 255, 0.78);
+ border-radius: 10px;
+}
+
+.console-v2 .v2-transport-field > .field-label {
+ margin-bottom: 6px;
+}
+
+.console-v2 .v2-transport-field .vehicle-transport-options {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 5px;
+}
+
+.console-v2 .v2-transport-field .mode-option {
+ min-width: 0;
+ min-height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ padding: 5px 7px;
+ color: #657879;
+ background: rgba(246, 249, 248, 0.92);
+ border: 1px solid rgba(105, 137, 137, 0.16);
+ border-radius: 8px;
+ font-size: 10px;
+ font-weight: 850;
+ cursor: pointer;
+}
+
+.console-v2 .v2-transport-field .mode-option svg {
+ width: 14px;
+ height: 14px;
+}
+
+.console-v2 .v2-transport-field .mode-option.active {
+ color: #fff;
+ background: linear-gradient(145deg, var(--v2-accent), var(--v2-accent-strong));
+ border-color: transparent;
+ box-shadow: 0 5px 13px color-mix(in srgb, var(--v2-accent) 20%, transparent);
+}
+
+.console-v2 .v2-transport-field > #vehicleTransportNote {
+ display: block;
+ margin: 6px 1px 0;
+ overflow: hidden;
+ color: #809091;
+ font-size: 8px;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.console-v2 .v2-config-card {
min-width: 0;
min-height: var(--v2-sidebar-card-height);
diff --git a/apps/web-console/console-v2.js b/apps/web-console/console-v2.js
index ba4b9ba..b942e89 100644
--- a/apps/web-console/console-v2.js
+++ b/apps/web-console/console-v2.js
@@ -293,6 +293,7 @@
function buildConfigFields(configStack, originalForm) {
const vehicleField = $(".vehicle-field", originalForm);
+ const transportField = $("#vehicleTransportField", originalForm);
const providerField = $(".field-group:has(.segmented [data-provider])", originalForm);
const reasoningField = $("#reasoningField", originalForm);
const executionField = $(".field-group:has(.execution-mode-options)", originalForm);
@@ -319,6 +320,11 @@
configStack.append(vehicleField);
}
+ if (transportField) {
+ transportField.classList.add("v2-config-field", "v2-transport-field");
+ configStack.append(transportField);
+ }
+
const modelField = make("div", "v2-config-field v2-config-card v2-model-field", `
推理模型模型检测中
diff --git a/apps/web-console/index.html b/apps/web-console/index.html
index a1e2a3a..2c15c85 100755
--- a/apps/web-console/index.html
+++ b/apps/web-console/index.html
@@ -9,18 +9,18 @@
511工程-智能座舱
-
-
+
+
-
+
-
+
@@ -140,6 +140,19 @@
所有座椅能力仅控制副驾实体电机、加热与通风执行器
+
+
车辆连接方式
+
+
+
+
+
正在检查车辆连接
+
+