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 @@

所有座椅能力仅控制副驾实体电机、加热与通风执行器

+
+ 车辆连接方式 +
+ + +
+

正在检查车辆连接

+
+