merge: integrate wired and wireless CAN into N7Web

This commit is contained in:
minghanchen
2026-08-19 10:21:27 +08:00
34 changed files with 4740 additions and 246 deletions
+194 -42
View File
@@ -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
? '<i data-lucide="shield-check"></i>CH1 车端已配置'
? `<i data-lucide="shield-check"></i>${mqttSelected ? "MQTT" : "CH1"} 车端已配置`
: linkedHardwareReady
? '<i data-lucide="shield-check"></i>双通道已配置'
: seatHardwareReady
? '<i data-lucide="shield-check"></i>座椅台架已配置'
: '<i data-lucide="shield-off"></i>硬件锁定';
$("#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();
}
+57
View File
@@ -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);
+6
View File
@@ -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", `
<i data-lucide="sparkles"></i>
<div><small>推理模型</small><strong id="v2ModelValue">模型检测中</strong></div>
+17 -4
View File
@@ -9,18 +9,18 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/app-icon-192.png" />
<title>511工程-智能座舱</title>
<link rel="stylesheet" href="/styles.css?v=dual-seat-9" />
<link rel="stylesheet" href="/console-v2.css?v=sync-layout-26" />
<link rel="stylesheet" href="/styles.css?v=dual-seat-10" />
<link rel="stylesheet" href="/console-v2.css?v=sync-layout-27" />
<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/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/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-1" defer></script>
<script src="/app.js?v=dual-seat-85" defer></script>
<script src="/app.js?v=dual-seat-90" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-8"></script>
<script src="/pwa.js?v=android-pad-2" defer></script>
<script src="/console-v2.js?v=sync-layout-26" defer></script>
<script src="/console-v2.js?v=sync-layout-27" defer></script>
<script src="/self-extension.js?v=sync-layout-26" defer></script>
</head>
<body>
@@ -140,6 +140,19 @@
<p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p>
</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>有线控制
</button>
<button type="button" class="mode-option" data-control-transport="wireless_mqtt">
<i data-lucide="wifi"></i>无线控制
</button>
</div>
<p class="provider-note" id="vehicleTransportNote">正在检查车辆连接</p>
</div>
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
<label class="field-label" for="simulationFault">故障注入场景</label>
<select id="simulationFault" aria-label="选择模拟硬件故障">
+5 -5
View File
@@ -1,16 +1,16 @@
const CACHE_VERSION = "cognitive-agent-pad-v178";
const CACHE_VERSION = "cognitive-agent-pad-v179";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
"/",
"/index.html",
"/styles.css?v=dual-seat-9",
"/styles.css?v=dual-seat-10",
"/network.js?v=android-pad-1",
"/app.js?v=dual-seat-85",
"/app.js?v=dual-seat-90",
"/gesture-control.js?v=android-pad-8",
"/pwa.js?v=android-pad-2",
"/console-v2.css?v=sync-layout-26",
"/console-v2.js?v=sync-layout-26",
"/console-v2.css?v=sync-layout-27",
"/console-v2.js?v=sync-layout-27",
"/self-extension.css?v=sync-layout-26",
"/self-extension.js?v=sync-layout-26",
"/assets/pets/self-extension-cat.png?v=3",
+18
View File
@@ -5071,6 +5071,24 @@ pre {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.vehicle-transport-options {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.vehicle-transport-options .mode-option[data-connection-state="offline"] {
color: var(--muted);
}
.vehicle-transport-options .mode-option[data-connection-state="online"]::after {
content: "";
width: 7px;
height: 7px;
margin-left: 4px;
border-radius: 50%;
background: #2ccf83;
box-shadow: 0 0 8px rgba(44, 207, 131, 0.65);
}
.simulation-fault-field select {
width: 100%;
min-height: 44px;