优化了副驾座椅控制

This commit is contained in:
minghanchen
2026-08-25 11:39:41 +08:00
parent 33c92d1352
commit 9ac8fdface
34 changed files with 31714 additions and 289 deletions
+164 -56
View File
@@ -80,8 +80,8 @@
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_longitudinal_position": ["副驾座椅前后调节", "horizontal", 0],
"seat.set_cushion_tilt": ["副驾座椅高度调节", "basin", 1],
"seat.set_backrest_recline": ["副驾座椅靠背调节", "backrest", 2],
"seat.set_legrest_height": ["副驾座椅腿托调节", "legrest", 3],
};
@@ -89,13 +89,38 @@
const position = boundedInteger(parameters.position_pct, 0, 100);
if (axis && position !== null) {
const targetPermille = position * 10;
const motion = operation.motion || {};
const delta = boundedInteger(motion.delta_position_pct, 1, 100);
const relativeDirections = {
horizontal: { increase: "backward", decrease: "forward" },
basin: { increase: "down", decrease: "up" },
backrest: { increase: "backward", decrease: "forward" },
legrest: { increase: "up", decrease: "down" },
};
const relativeDirection = relativeDirections[axis[1]]?.[motion.direction];
return command(
axis[0],
`seat passenger ${axis[1]} position ${position}`,
motion.mode === "relative" && relativeDirection && delta !== null
? `seat passenger ${axis[1]} relative ${relativeDirection} ${delta}`
: `seat passenger ${axis[1]} position ${position}`,
"CAN0",
otaCan0Frame(0x64, axis[2], targetPermille),
);
}
if (new Set(["seat.set_ventilation", "seat.set_heating"]).has(capability)) {
const feature = capability === "seat.set_heating" ? "heat" : "vent";
const level = boundedInteger(parameters.level, 1, 3);
const setting = onOff === "off"
? "off"
: ({ 1: "low", 2: "medium", 3: "high" })[level];
if (setting) {
return command(
"副驾座椅通风/加热",
`seat passenger ${feature} ${setting}`,
"CAN0",
);
}
}
if (capability === "seat.start_dynamic_mode") {
const mode = boundedInteger(parameters.mode_id, 1, 2);
if (mode !== null) {
@@ -104,7 +129,7 @@
: { name: "cradle", selector: 1 };
return command(
"副驾座椅动态模式",
`seat passenger dynamic start ${dynamic.name}`,
`seat passenger dynamic ${dynamic.name}`,
"CAN0",
otaCan0Frame(0x66, dynamic.selector, 300),
);
@@ -301,7 +326,7 @@
<li data-can-channel="${escapeHtml(item.channel)}">
<span>
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${item.parseSource === "local_rule" ? "本地规则" : "云端模型"}</b>` : ""}
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${item.parseSource === "direct_control" ? "设备控件" : item.parseSource === "local_rule" ? "本地规则" : "云端模型"}</b>` : ""}
${escapeHtml(item.category)}
</span>
<code>${escapeHtml(item.command)}</code>
@@ -313,6 +338,30 @@
requestAnimationFrame(syncSidebarLogBaseline);
}
function renderDirectCanCommands(commands, deviceName) {
const panel = $("#v2CanCommandPanel");
const list = $("#v2CanCommandList");
const count = $("#v2CanCommandCount");
if (!panel || !list || !count) return;
const items = commands.map((command) => ({
channel: command.startsWith("seat passenger ") ? "CAN0" : "CAN1",
command,
}));
count.textContent = String(items.length);
panel.dataset.state = "ready";
list.innerHTML = `<ol>${items.map((item) => `
<li data-can-channel="${item.channel}">
<span>
<b class="v2-can-command-channel">${item.channel}</b>
<b class="v2-can-command-source" data-parse-source="direct_control">设备控件</b>
${escapeHtml(deviceName)}
</span>
<code>${escapeHtml(item.command)}</code>
</li>`).join("")}</ol>`;
validatedCommandsVisible = true;
requestAnimationFrame(syncSidebarLogBaseline);
}
function closeReasoningPicker() {
reasoningPickerControl?.close();
}
@@ -620,7 +669,7 @@
function seatLevelControlValue(seat, feature) {
const text = seatFunctionValue(seat, feature);
const level = text.match(/L(\d+)/i)?.[1];
return /OFF|关闭|--/.test(text) ? "off" : level || "1";
return /OFF|关闭|--/.test(text) ? "off" : level === "2" ? "low" : "high";
}
function massageReadout() {
@@ -632,34 +681,31 @@
}
const levelOptions = [
["off", "关闭"], ["1", "1 档"], ["2", "2 档"], ["3", "3 档"],
["off", "关闭"], ["high", "1 档(高)"], ["low", "2 档(低)"],
];
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
const massageModeOptions = [
["off", "关闭"], ["relax", "舒缓"], ["stretch", "伸展"], ["waist", "腰部"], ["wave", "波浪"],
["off", "关闭"], ["relax", "放松"], ["stretch", "伸展"], ["slack", "松弛"], ["wave", "波浪"], ["pulse", "脉冲"],
];
const massageStrengthOptions = [
["off", "关闭"], ["low", "低"], ["medium", "中"], ["high", "高"],
["off", "关闭"], ["weak", "低"], ["medium", "中"], ["strong", "高"],
];
const colorIndexNames = ["皓月白", "冰川蓝", "松石绿", "星云紫", "琥珀橙", "玫瑰红"];
const colorIndexOptions = Array.from({ length: 16 }, (_item, index) => [
String(index), colorIndexNames[index] ? `${index} · ${colorIndexNames[index]}` : `色号 ${index}`,
const colorIndexOptions = Array.from({ length: 127 }, (_item, index) => [
String(index + 1), colorIndexNames[index] ? `${index + 1} · ${colorIndexNames[index]}` : `色号 ${index + 1}`,
]);
const rangeControl = (key, label, value, min = 0, max = 100, step = 1, unit = "%", hint = "") => ({
key, label, type: "range", value: clampControlNumber(value, min, max), min, max, step, unit, hint,
});
const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options });
const hasControlValue = (values, key) => Object.hasOwn(values, key);
function deviceControlValue(deviceId, control) {
const draft = displaySync.deviceDrafts[deviceId];
return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value;
}
function optionLabel(options, value) {
return options.find(([optionValue]) => String(optionValue) === String(value))?.[1] || String(value);
}
const syncedDevices = [
{
id: "passenger", name: "副驾座椅", icon: "armchair",
@@ -670,7 +716,13 @@
rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")),
rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")),
],
command: (values) => `调节副驾座椅:前后${values.longitudinal}%,座椅高度${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`,
commands: (values) => [
["longitudinal", "horizontal"],
["cushion_tilt", "basin"],
["backrest_recline", "backrest"],
["legrest_height", "legrest"],
].filter(([key]) => hasControlValue(values, key))
.map(([key, axis]) => `seat passenger ${axis} position ${Math.round(Number(values[key]))}`),
},
{
id: "driver", name: "主驾座椅", icon: "armchair",
@@ -687,7 +739,22 @@
selectControl("massage_strength", "按摩强度", /关闭|--/.test(massageStrength) ? "off" : massageStrength.toLowerCase(), massageStrengthOptions),
];
},
command: (values) => `调节主驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},按摩模式${optionLabel(massageModeOptions, values.massage_mode)},按摩强度${optionLabel(massageStrengthOptions, values.massage_strength)}`,
commands: (values) => {
const commands = [];
if (hasControlValue(values, "ventilation")) commands.push(`seat driver vent ${values.ventilation}`);
if (hasControlValue(values, "heating")) commands.push(`seat driver heat ${values.heating}`);
if (hasControlValue(values, "massage_mode")) {
commands.push(values.massage_mode === "off"
? "seat driver massage off"
: `seat driver massage mode ${values.massage_mode}`);
}
if (hasControlValue(values, "massage_strength")) {
commands.push(values.massage_strength === "off"
? "seat driver massage off"
: `seat driver massage strength ${values.massage_strength}`);
}
return [...new Set(commands)];
},
},
{
id: "windows", name: "车窗", icon: "gallery-vertical-end",
@@ -698,19 +765,24 @@
rangeControl("rear_left", "左后窗", readText("#rearLeftWindow")),
rangeControl("rear_right", "右后窗", readText("#rearRightWindow")),
],
command: (values) => `调节车窗开合比例:左前窗${values.front_left}%,右前窗${values.front_right}%,左后窗${values.rear_left}%,右后窗${values.rear_right}%`,
commands: (values) => Object.entries({
front_left: "front-left", front_right: "front-right", rear_left: "rear-left", rear_right: "rear-right",
}).filter(([key]) => hasControlValue(values, key))
.map(([key, target]) => `window ${target} position ${Math.round(Number(values[key]))}`),
},
{
id: "shade", name: "遮阳帘", icon: "panels-top-left",
rows: () => [["当前位置", readText("#shadePositionValue")]],
controls: () => [rangeControl("position", "当前位置", readText("#shadePositionValue"), 0, 100, 1, "%", "0% 关闭 · 100% 打开")],
command: (values) => `将遮阳帘开合位置调整为${values.position}%`,
commands: (values) => hasControlValue(values, "position")
? [`shade position ${Math.round(Number(values.position))}`]
: [],
},
{
id: "reading", name: "阅读灯", icon: "book-open",
rows: () => [["开关状态", readText("#interiorLightPower", "等待反馈")]],
controls: () => [selectControl("power", "开关状态", /开启|ON/.test(readText("#interiorLightPower")) ? "on" : "off", toggleOptions)],
command: (values) => `${values.power === "on" ? "打开" : "关闭"}阅读灯`,
commands: (values) => hasControlValue(values, "power") ? [`light reading all ${values.power}`] : [],
},
{
id: "ambient", name: "氛围灯", icon: "sparkles",
@@ -724,31 +796,39 @@
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
return [
selectControl("power", "开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "off" : "on", toggleOptions),
rangeControl("front_brightness", "前排亮度", frontBrightness, 0, 5, 1, " 级"),
selectControl("front_color", "前排颜色", String(clampControlNumber(frontColor, 0, 15, 0)), colorIndexOptions),
rangeControl("rear_brightness", "后排亮度", rearBrightness, 0, 5, 1, " 级"),
selectControl("rear_color", "后排颜色", String(clampControlNumber(rearColor, 0, 15, 0)), colorIndexOptions),
rangeControl("brightness", "全车亮度", frontBrightness === "-" ? rearBrightness : frontBrightness, 1, 5, 1, " 级"),
selectControl("color", "全车颜色", String(clampControlNumber(frontColor === "-" ? rearColor : frontColor, 1, 127, 1)), colorIndexOptions),
];
},
command: (values) => `调节氛围灯:${values.power === "on" ? "开启" : "关闭"},前排亮度${values.front_brightness}级、颜色${optionLabel(colorIndexOptions, values.front_color)},后排亮度${values.rear_brightness}级、颜色${optionLabel(colorIndexOptions, values.rear_color)}`,
commands: (values) => [
...(hasControlValue(values, "power") ? [`light ambient ${values.power}`] : []),
...(hasControlValue(values, "brightness") ? [`light ambient brightness ${Math.round(Number(values.brightness))}`] : []),
...(hasControlValue(values, "color") ? [`light ambient color ${Math.round(Number(values.color))}`] : []),
],
},
{
id: "headlight", name: "前大灯", icon: "car-front",
controllable: false,
rows: () => [["近光灯", readText("#lowBeamState")], ["远光灯", readText("#highBeamState")], ["雾灯", readText("#fogLightState")], ["转向灯", readText("#turnSignalState")]],
controls: () => [["low_beam", "近光灯", "#lowBeamState"], ["high_beam", "远光灯", "#highBeamState"], ["fog_light", "雾灯", "#fogLightState"], ["turn_signal", "转向灯", "#turnSignalState"]].map(([key, label, selector]) => selectControl(key, label, /开启|ON/.test(readText(selector)) ? "on" : "off", toggleOptions)),
command: (values) => `调节前大灯:近光灯${optionLabel(toggleOptions, values.low_beam)},远光灯${optionLabel(toggleOptions, values.high_beam)},雾灯${optionLabel(toggleOptions, values.fog_light)},转向灯${optionLabel(toggleOptions, values.turn_signal)}`,
},
{
id: "climate", name: "空调", icon: "fan",
rows: () => [["开关状态", readText("#climatePower", "等待反馈")], ["主驾温度", readText("#driverTemperature")], ["副驾温度", readText("#passengerTemperature")], ["风量", readText("#fanLevel")], ["循环模式", readText("#cycleMode")]],
controls: () => [
selectControl("power", "开关状态", /ON|ACTIVE|开启/.test(readText("#climatePower")) ? "on" : "off", toggleOptions),
rangeControl("driver_temperature", "主驾温度", readText("#driverTemperature"), 16, 32, 0.5, "℃"),
rangeControl("passenger_temperature", "副驾温度", readText("#passengerTemperature"), 16, 32, 0.5, "℃"),
rangeControl("fan_level", "风量", readText("#fanLevel"), 0, 7, 1, " 档"),
rangeControl("driver_temperature", "主驾温度", Math.round(clampControlNumber(readText("#driverTemperature"), 18, 32, 24)), 18, 32, 1, "℃"),
rangeControl("passenger_temperature", "副驾温度", Math.round(clampControlNumber(readText("#passengerTemperature"), 18, 32, 24)), 18, 32, 1, "℃"),
rangeControl("fan_level", "风量", readText("#fanLevel"), 1, 7, 1, " 档"),
selectControl("cycle_mode", "循环模式", readText("#cycleMode").includes("内") ? "internal" : "external", [["internal", "内循环"], ["external", "外循环"]]),
],
command: (values) => `调节空调:${values.power === "on" ? "开启" : "关闭"},主驾${values.driver_temperature}℃,副驾${values.passenger_temperature}℃,风量${values.fan_level}档,${values.cycle_mode === "internal" ? "内循环" : "外循环"}`,
commands: (values) => [
...(hasControlValue(values, "power") ? [`ac ${values.power}`] : []),
...(hasControlValue(values, "driver_temperature") ? [`ac temp driver ${Math.round(Number(values.driver_temperature))}`] : []),
...(hasControlValue(values, "passenger_temperature") ? [`ac temp passenger ${Math.round(Number(values.passenger_temperature))}`] : []),
...(hasControlValue(values, "fan_level") ? [`ac fan ${Math.round(Number(values.fan_level))}`] : []),
...(hasControlValue(values, "cycle_mode") ? [`ac cycle ${values.cycle_mode}`] : []),
],
},
];
@@ -771,7 +851,8 @@
grid.innerHTML = syncedDevices.map((device) => {
const rows = device.rows();
const waiting = rows.some(([, value]) => /--|等待|同步中|未知/.test(value));
const controlOpen = displaySync.controlDevice === device.id;
const controllable = device.controllable !== false;
const controlOpen = controllable && displaySync.controlDevice === device.id;
const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
const pending = Boolean(displaySync.pendingControls[device.id]);
const stateLabel = pending ? "已发送 · 等待同步" : hasDraft ? "参数待发送" : `${rows.length} 项关键状态`;
@@ -780,13 +861,13 @@
<button class="v2-device-card-focus" type="button" data-device-focus="${device.id}" aria-label="在孪生界面查看${device.name}">
<span><i data-lucide="${device.icon}"></i></span><span><strong>${device.name}</strong><small data-device-card-state>${stateLabel}</small></span><i class="v2-device-signal"></i>
</button>
<button class="v2-device-adjust-toggle" type="button" data-device-control-toggle="${device.id}" aria-expanded="${String(controlOpen)}"><i data-lucide="sliders-horizontal"></i><span>${controlOpen ? "收起" : "调节"}</span></button>
<button class="v2-device-adjust-toggle" type="button" data-device-control-toggle="${device.id}" aria-expanded="${String(controlOpen)}"${controllable ? "" : " disabled"}><i data-lucide="${controllable ? "sliders-horizontal" : "eye"}"></i><span>${controllable ? (controlOpen ? "收起" : "调节") : "只读"}</span></button>
</header>
<dl>${rows.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>
<div class="v2-device-controls" data-device-controls="${device.id}"${controlOpen ? "" : " hidden"}>
<div class="v2-device-controls-intro"><span>手动调节</span><small>参数确认后通过编排链路发送至车机</small></div>
${device.controls().map((control) => renderDeviceControl(device, control)).join("")}
<button class="v2-device-control-apply" type="button" data-device-control-apply="${device.id}"${pending ? " disabled" : ""}><i data-lucide="send"></i><span>${pending ? "等待车机同步" : "发送控制"}</span></button>
<button class="v2-device-control-apply" type="button" data-device-control-apply="${device.id}"${pending || !hasDraft ? " disabled" : ""}><i data-lucide="send"></i><span>${pending ? "等待车机同步" : hasDraft ? "发送 CAN 控制" : "请先修改参数"}</span></button>
</div>
</article>`;
}).join("");
@@ -817,28 +898,68 @@
}));
}
function sendDeviceControl(deviceId) {
async function sendDeviceControl(deviceId) {
const device = syncedDevices.find((item) => item.id === deviceId);
if (!device) return;
const values = Object.fromEntries(device.controls().map((control) => [control.key, String(deviceControlValue(device.id, control))]));
const utterance = device.command(values);
if (device.controllable === false || typeof device.commands !== "function") {
window.CognitiveAgentConsole?.notify?.(`${device.name}当前只有状态反馈,尚无已验证的 CAN 控制白名单`);
return;
}
const draft = displaySync.deviceDrafts[device.id];
if (!draft || !Object.keys(draft).length) {
window.CognitiveAgentConsole?.notify?.("请先修改需要发送的参数");
return;
}
if (displaySync.pendingControls[device.id]) {
window.CognitiveAgentConsole?.notify?.("该设备的上一组 CAN 指令仍在执行");
return;
}
const values = Object.fromEntries(Object.entries(draft).map(([key, value]) => [key, String(value)]));
const commands = device.commands(values).filter(Boolean);
if (!commands.length) {
window.CognitiveAgentConsole?.notify?.("修改项没有对应的 CAN 白名单控制指令");
return;
}
const commandSummary = commands.join(";");
const detail = {
device_id: device.id,
device_name: device.name,
controls: values,
utterance,
can_commands: commands,
source: "web_console",
requested_at: new Date().toISOString(),
};
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-request", { detail }));
const accepted = window.CognitiveAgentConsole?.executeUtterance?.(utterance, { autoSubmit: true }) || false;
if (!accepted) {
window.CognitiveAgentConsole?.notify?.("上一条指令仍在执行,请稍候");
const execute = window.CognitiveAgentConsole?.executeCanCommands;
if (typeof execute !== "function") {
window.CognitiveAgentConsole?.notify?.("当前页面尚未加载 CAN 直控接口");
return;
}
displaySync.pendingControls[device.id] = utterance;
appendSyncLog({ category: "device", title: `网页端调节${device.name}`, detail: utterance, status: "info" });
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-can-control-request", { detail }));
displaySync.pendingControls[device.id] = commandSummary;
appendSyncLog({ category: "device", title: `网页端直控${device.name}`, detail: commandSummary, status: "info" });
renderDirectCanCommands(commands, device.name);
renderSyncedDevices();
try {
await execute(commands, { deviceName: device.name });
delete displaySync.deviceDrafts[device.id];
appendSyncLog({
category: "device",
title: `${device.name} CAN 控制已执行`,
detail: commandSummary,
status: "success",
});
} catch (error) {
appendSyncLog({
category: "device",
title: `${device.name} CAN 控制发送失败`,
detail: error?.message || "CAN 控制请求失败",
status: "error",
});
window.CognitiveAgentConsole?.notify?.(error?.message || "CAN 控制请求失败");
} finally {
delete displaySync.pendingControls[device.id];
renderSyncedDevices();
}
}
function scheduleSyncedDeviceRender() {
@@ -1389,19 +1510,6 @@
});
});
window.addEventListener("cognitive-agent:request-complete", (event) => {
const pendingDevice = Object.entries(displaySync.pendingControls).find(([, utterance]) => utterance === event.detail?.utterance)?.[0];
if (pendingDevice) {
const device = syncedDevices.find((item) => item.id === pendingDevice);
delete displaySync.pendingControls[pendingDevice];
if (!event.detail?.error) delete displaySync.deviceDrafts[pendingDevice];
appendSyncLog({
category: "device",
title: event.detail?.error ? `${device?.name || "设备"}控制发送失败` : `${device?.name || "设备"}控制已执行`,
detail: event.detail?.error?.message || "控制结果已进入车机状态同步链路",
status: event.detail?.error ? "error" : "success",
});
renderSyncedDevices();
}
if (event.detail?.error && !validatedCommandsVisible) {
renderParsedCanCommands(null, { message: "本次请求未生成 CAN 控制指令" });
}