优化了副驾座椅控制

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
+847
View File
@@ -0,0 +1,847 @@
#!/usr/bin/env python3
"""Generate the expanded natural-language to CAN command test catalog."""
from __future__ import annotations
import csv
from collections import Counter
from itertools import combinations
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
JSON_PATH = ROOT / "tests/fixtures/natural_can_command_combination_suite.json"
CSV_PATH = ROOT / "docs/testing/自然语言转CAN组合测试用例.csv"
DOC_PATH = ROOT / "docs/testing/自然语言转CAN组合测试说明.md"
def can0_operation(
capability: str,
parameters: dict[str, Any],
target: str = "passenger",
) -> dict[str, Any]:
return {
"target": target,
"capability": capability,
"parameters": parameters,
}
def build_cases() -> list[dict[str, Any]]:
cases: list[dict[str, Any]] = []
def add(
case_id: str,
utterance: str,
*,
domains: tuple[str, ...],
channels: tuple[str, ...] = ("can1",),
commands: tuple[str, ...] = (),
can0_operations: tuple[dict[str, Any], ...] = (),
kind: str = "atomic",
status: str = "succeeded",
sources: tuple[str, ...] = (),
) -> dict[str, Any]:
case = {
"id": case_id,
"kind": kind,
"channels": list(channels),
"domains": list(domains),
"utterance": utterance,
"expected_status": status,
"expected_commands": list(commands),
"expected_can0_operations": list(can0_operations),
}
if sources:
case["source_case_ids"] = list(sources)
cases.append(case)
return case
add(
"CAN1-DOOR-REAR-LOCK",
"后排车门上锁",
domains=("door",),
commands=("door rear lock",),
)
add(
"CAN1-DOOR-REAR-UNLOCK",
"后排门锁解锁",
domains=("door",),
commands=("door rear unlock",),
)
window_targets = {
"ALL": ("全部车窗", "all"),
"FRONT": ("前排车窗", "front"),
"REAR": ("后排车窗", "rear"),
"FRONT-LEFT": ("左前车窗", "front-left"),
"FRONT-RIGHT": ("右前车窗", "front-right"),
"REAR-LEFT": ("左后车窗", "rear-left"),
"REAR-RIGHT": ("右后车窗", "rear-right"),
}
window_motions = {
"OPEN": ("打开", "open"),
"CLOSE": ("关闭", "close"),
"STOP": ("停止", "stop"),
}
for target_id, (target_text, target_command) in window_targets.items():
for motion_id, (verb, motion) in window_motions.items():
add(
f"CAN1-WINDOW-{target_id}-{motion_id}",
f"{verb}{target_text}",
domains=("window",),
commands=(f"window {target_command} {motion}",),
)
for target_id, (target_text, target_command) in tuple(window_targets.items())[3:]:
for position in (0, 25, 50, 100):
add(
f"CAN1-WINDOW-{target_id}-POSITION-{position}",
f"把{target_text}开到{position}%",
domains=("window",),
commands=(f"window {target_command} position {position}",),
)
for alias_id, alias, target in (
("DRIVER", "主驾", "front-left"),
("PASSENGER", "副驾", "front-right"),
):
for motion_id, (verb, motion) in window_motions.items():
add(
f"CAN1-WINDOW-{alias_id}-ALIAS-{motion_id}",
f"{verb}{alias}车窗",
domains=("window",),
commands=(f"window {target} {motion}",),
)
add("CAN1-AC-ON", "打开空调", domains=("climate",), commands=("ac on",))
add("CAN1-AC-OFF", "关闭空调", domains=("climate",), commands=("ac off",))
for level in range(1, 8):
add(
f"CAN1-AC-FAN-{level}",
f"空调风量调到{level}档",
domains=("climate",),
commands=(f"ac fan {level}",),
)
for alias_id, alias, level in (
("MIN", "最小", 1),
("LOWEST", "最低", 1),
("LOW", "低档", 1),
("MIDDLE", "中等", 4),
("MEDIUM", "中档", 4),
("HIGH", "高档", 7),
("HIGHEST", "最高", 7),
("MAX", "最大", 7),
):
add(
f"CAN1-AC-FAN-ALIAS-{alias_id}",
f"空调风量设置为{alias}",
domains=("climate",),
commands=(f"ac fan {level}",),
)
outlet_modes = (
("FACE", "吹脸", "face"),
("FACE-FEET", "吹面和吹脚", "face-feet"),
("FEET", "吹脚", "feet"),
("FEET-DEFROST", "吹脚除霜", "feet-defrost"),
("DEFROST", "除霜", "defrost"),
("FACE-FEET-DEFROST", "吹脸、吹脚和除霜", "face-feet-defrost"),
("FACE-DEFROST", "吹脸和除霜", "face-defrost"),
)
for mode_id, mode_text, mode in outlet_modes:
add(
f"CAN1-AC-MODE-{mode_id}",
f"空调出风模式设置为{mode_text}",
domains=("climate",),
commands=(f"ac mode {mode}",),
)
for target_id, target_text, target in (
("DRIVER", "主驾", "driver"),
("PASSENGER", "副驾", "passenger"),
("REAR", "后排", "rear"),
):
for temperature in (18, 24, 32):
add(
f"CAN1-AC-TEMP-{target_id}-{temperature}",
f"{target_text}空调温度设置为{temperature}度",
domains=("climate",),
commands=(f"ac temp {target} {temperature}",),
)
for mode_id, mode_text, mode in (
("MANUAL", "手动控制", "manual"),
("AUTO", "自动控制", "auto"),
):
add(
f"CAN1-AC-CONTROL-{mode_id}",
f"空调设置为{mode_text}",
domains=("climate",),
commands=(f"ac control {mode}",),
)
for cycle_id, cycle_text, cycle in (
("INTERNAL", "内循环", "internal"),
("EXTERNAL", "外循环", "external"),
):
add(
f"CAN1-AC-CYCLE-{cycle_id}",
f"空调设置为{cycle_text}",
domains=("climate",),
commands=(f"ac cycle {cycle}",),
)
outlet_targets = (
("DRIVER-LEFT", "主驾左", "driver-left", (-25, -40)),
("DRIVER-RIGHT", "主驾右", "driver-right", (30, 30)),
("PASSENGER-LEFT", "副驾左", "passenger-left", (-10, -40)),
("PASSENGER-RIGHT", "副驾右", "passenger-right", (5, 30)),
)
for target_id, target_text, target, calibrated in outlet_targets:
for horizontal, vertical in ((0, 0), (25, 75), (100, 100)):
add(
f"CAN1-OUTLET-{target_id}-PERCENT-{horizontal}-{vertical}",
f"{target_text}风口水平{horizontal}%上下{vertical}%",
domains=("outlet",),
commands=(
f"outlet {target} percent {horizontal} {vertical}",
),
)
add(
f"CAN1-OUTLET-{target_id}-CALIBRATED",
f"{target_text}风口标定坐标左右{calibrated[0]}上下{calibrated[1]}",
domains=("outlet",),
commands=(
f"outlet {target} position {calibrated[0]} {calibrated[1]}",
),
)
for feature_id, feature_text, feature in (
("VENT", "通风", "vent"),
("HEAT", "加热", "heat"),
):
for level_id, level_text, level in (
("OFF", "关闭", "off"),
("LOW", "低档", "low"),
("MIN", "最低", "low"),
("HIGH", "高档", "high"),
("MAX", "最大", "high"),
):
utterance = (
f"关闭主驾座椅{feature_text}"
if level == "off"
else f"主驾座椅{feature_text}设置为{level_text}"
)
add(
f"CAN1-SEAT-DRIVER-{feature_id}-{level_id}",
utterance,
domains=("seat",),
commands=(f"seat driver {feature} {level}",),
)
add(
f"CAN1-SEAT-DRIVER-{feature_id}-OPEN-DEFAULT",
f"打开主驾座椅{feature_text}",
domains=("seat",),
commands=(f"seat driver {feature} low",),
)
add(
"CAN1-SEAT-DRIVER-MASSAGE-OPEN",
"打开主驾座椅按摩",
domains=("seat",),
commands=("seat driver massage mode relax",),
)
add(
"CAN1-SEAT-DRIVER-MASSAGE-OFF",
"关闭主驾座椅按摩",
domains=("seat",),
commands=("seat driver massage off",),
)
for strength_id, strength_text, strength in (
("WEAK", "最小", "weak"),
("MEDIUM", "中等", "medium"),
("STRONG", "最大", "strong"),
("LOWEST", "最低", "weak"),
("HIGHEST", "最高", "strong"),
):
add(
f"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-{strength_id}",
f"主驾座椅按摩强度设置为{strength_text}",
domains=("seat",),
commands=(f"seat driver massage strength {strength}",),
)
for mode_id, mode_text, mode in (
("WAVE", "波浪", "wave"),
("PULSE", "脉冲", "pulse"),
("STRETCH", "伸展", "stretch"),
("SLACK", "松弛", "slack"),
("RELAX", "放松", "relax"),
):
add(
f"CAN1-SEAT-DRIVER-MASSAGE-MODE-{mode_id}",
f"主驾座椅按摩模式设置为{mode_text}",
domains=("seat",),
commands=(f"seat driver massage mode {mode}",),
)
driver_motion_cases = (
("HORIZONTAL-FORWARD", "主驾座椅向前移动500毫秒", "horizontal forward 500"),
("HORIZONTAL-BACKWARD", "主驾座椅后退两秒", "horizontal backward 2000"),
("HORIZONTAL-STOP", "停止主驾座椅前后移动", "horizontal stop"),
("HEIGHT-UP", "主驾座椅往上移动300毫秒", "height up 300"),
("HEIGHT-DOWN", "主驾座椅向下移动一秒", "height down 1000"),
("HEIGHT-STOP", "停止主驾座椅高度调节", "height stop"),
("BACKREST-FORWARD", "主驾座椅靠背调直500毫秒", "backrest forward 500"),
("BACKREST-BACKWARD", "主驾座椅椅背放倒800毫秒", "backrest backward 800"),
("BACKREST-STOP", "停止主驾座椅靠背动作", "backrest stop"),
("BASIN-UP", "主驾座椅坐盆抬高300毫秒", "basin up 300"),
("BASIN-DOWN", "主驾座椅座垫放低一秒", "basin down 1000"),
("BASIN-STOP", "停止主驾座椅坐盆动作", "basin stop"),
)
for case_id, utterance, suffix in driver_motion_cases:
add(
f"CAN1-SEAT-DRIVER-{case_id}",
utterance,
domains=("seat",),
commands=(f"seat driver {suffix}",),
)
add(
"CAN1-SEAT-DRIVER-STOP-ALL",
"停止主驾座椅移动",
domains=("seat",),
commands=(
"seat driver horizontal stop",
"seat driver height stop",
"seat driver backrest stop",
"seat driver basin stop",
),
)
for slot in (1, 2, 3):
add(
f"CAN1-SEAT-DRIVER-MEMORY-{slot}",
f"调用主驾座椅记忆位置{slot}",
domains=("seat",),
commands=(f"seat driver position {slot}",),
)
for state_id, state_text, state in (
("ON", "打开", "on"),
("OFF", "关闭", "off"),
):
add(
f"CAN1-LIGHT-INDOOR-{state_id}",
f"{state_text}室内灯",
domains=("interior_light",),
commands=(f"light indoor {state}",),
)
add(
f"CAN1-LIGHT-READING-ALL-{state_id}",
f"{state_text}全部阅读灯",
domains=("interior_light",),
commands=(f"light reading all {state}",),
)
add(
f"CAN1-LIGHT-AMBIENT-{state_id}",
f"{state_text}氛围灯",
domains=("ambient_light",),
commands=(f"light ambient {state}",),
)
for color in (1, 32, 70, 127):
add(
f"CAN1-LIGHT-AMBIENT-COLOR-{color}",
f"氛围灯颜色编号设置为{color}",
domains=("ambient_light",),
commands=(f"light ambient color {color}",),
)
for brightness in range(1, 6):
add(
f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-{brightness}",
f"氛围灯亮度设置为{brightness}档",
domains=("ambient_light",),
commands=(f"light ambient brightness {brightness}",),
)
for alias_id, alias, brightness in (
("MIN", "最低", 1),
("MIDDLE", "中等", 3),
("MAX", "最高", 5),
):
add(
f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-ALIAS-{alias_id}",
f"氛围灯亮度设置为{alias}",
domains=("ambient_light",),
commands=(f"light ambient brightness {brightness}",),
)
for motion_id, motion_text, motion in (
("OPEN", "打开", "open"),
("CLOSE", "关闭", "close"),
("HALF", "半开", "half"),
("STOP", "停止", "stop"),
):
utterance = f"遮阳帘{motion_text}" if motion == "half" else f"{motion_text}遮阳帘"
add(
f"CAN1-SHADE-{motion_id}",
utterance,
domains=("roof",),
commands=(f"shade {motion}",),
)
for position in (0, 20, 30, 50, 100):
add(
f"CAN1-SHADE-POSITION-{position}",
f"遮阳帘打开到{position}%",
domains=("roof",),
commands=(f"shade position {position}",),
)
can0_atoms = (
(
"CAN0-SEAT-PASSENGER-HEAT-1",
"副驾驶座椅加热开到1档",
can0_operation("seat.set_heating", {"state": "on", "level": 1}),
),
(
"CAN0-SEAT-PASSENGER-HEAT-2",
"副驾驶座椅加热开到2档",
can0_operation("seat.set_heating", {"state": "on", "level": 2}),
),
(
"CAN0-SEAT-PASSENGER-HEAT-3",
"副驾驶座椅加热开到3档",
can0_operation("seat.set_heating", {"state": "on", "level": 3}),
),
(
"CAN0-SEAT-PASSENGER-HEAT-OFF",
"关闭副驾驶座椅加热",
can0_operation("seat.set_heating", {"state": "off"}),
),
(
"CAN0-SEAT-PASSENGER-VENT-1",
"副驾驶座椅通风开到1档",
can0_operation("seat.set_ventilation", {"state": "on", "level": 1}),
),
(
"CAN0-SEAT-PASSENGER-VENT-2",
"副驾驶座椅通风开到2档",
can0_operation("seat.set_ventilation", {"state": "on", "level": 2}),
),
(
"CAN0-SEAT-PASSENGER-VENT-3",
"副驾驶座椅通风开到3档",
can0_operation("seat.set_ventilation", {"state": "on", "level": 3}),
),
(
"CAN0-SEAT-PASSENGER-VENT-OFF",
"关闭副驾驶座椅通风",
can0_operation("seat.set_ventilation", {"state": "off"}),
),
(
"CAN0-SEAT-PASSENGER-HORIZONTAL-30",
"把副驾驶座椅水平位置调到30%",
can0_operation("seat.set_longitudinal_position", {"position_pct": 30}),
),
(
"CAN0-SEAT-PASSENGER-BASIN-40",
"把副驾驶坐盆倾角调到40%",
can0_operation("seat.set_cushion_tilt", {"position_pct": 40}),
),
(
"CAN0-SEAT-PASSENGER-BACKREST-120",
"把副驾驶靠背调到120度",
can0_operation(
"seat.set_backrest_recline",
{"position_pct": 37, "angle_deg": 120},
),
),
(
"CAN0-SEAT-PASSENGER-LEGREST-125",
"把副驾驶腿托抬到125度",
can0_operation(
"seat.set_legrest_height",
{"position_pct": 42, "angle_deg": 125},
),
),
)
for case_id, utterance, operation in can0_atoms:
add(
case_id,
utterance,
domains=("seat",),
channels=("can0",),
can0_operations=(operation,),
)
atom_by_id = {case["id"]: case for case in cases}
def combine(
case_id: str,
source_ids: tuple[str, ...],
*,
separator: str = ",",
kind: str,
) -> None:
source_cases = [atom_by_id[source_id] for source_id in source_ids]
channels = tuple(
channel
for channel in ("can0", "can1")
if any(channel in item["channels"] for item in source_cases)
)
domains = tuple(
dict.fromkeys(
domain for item in source_cases for domain in item["domains"]
)
)
add(
case_id,
separator.join(item["utterance"] for item in source_cases),
domains=domains,
channels=channels,
commands=tuple(
command
for item in source_cases
for command in item["expected_commands"]
),
can0_operations=tuple(
operation
for item in source_cases
for operation in item["expected_can0_operations"]
),
kind=kind,
sources=source_ids,
)
pair_pool = (
"CAN1-DOOR-REAR-LOCK",
"CAN1-WINDOW-FRONT-LEFT-OPEN",
"CAN1-WINDOW-REAR-RIGHT-POSITION-25",
"CAN1-AC-ON",
"CAN1-AC-FAN-2",
"CAN1-AC-MODE-FACE-FEET",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-TEMP-PASSENGER-24",
"CAN1-AC-TEMP-REAR-24",
"CAN1-AC-CONTROL-AUTO",
"CAN1-AC-CYCLE-INTERNAL",
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
"CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75",
"CAN1-SEAT-DRIVER-VENT-HIGH",
"CAN1-SEAT-DRIVER-HEAT-LOW",
"CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX",
"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM",
"CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD",
"CAN1-SEAT-DRIVER-HEIGHT-UP",
"CAN1-LIGHT-INDOOR-ON",
"CAN1-LIGHT-READING-ALL-ON",
"CAN1-LIGHT-AMBIENT-ON",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-3",
"CAN1-SHADE-POSITION-30",
)
for index, source_pair in enumerate(combinations(pair_pool, 2), start=1):
combine(
f"COMBO-PAIR-COMMA-{index:03d}",
source_pair,
kind="pairwise",
)
combine(
f"COMBO-PAIR-SEMICOLON-{index:03d}",
source_pair,
separator=";",
kind="pairwise_separator_variant",
)
multi_anchors = (
"CAN1-DOOR-REAR-LOCK",
"CAN1-WINDOW-FRONT-LEFT-OPEN",
"CAN1-AC-FAN-2",
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
"CAN1-SEAT-DRIVER-VENT-HIGH",
"CAN1-LIGHT-INDOOR-ON",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-SHADE-POSITION-30",
"CAN0-SEAT-PASSENGER-HEAT-2",
)
for index, source_ids in enumerate(combinations(multi_anchors, 3), start=1):
combine(
f"COMBO-TRIPLE-{index:03d}",
source_ids,
kind="three_command",
)
for index, source_ids in enumerate(combinations(multi_anchors, 4), start=1):
combine(
f"COMBO-QUADRUPLE-{index:03d}",
source_ids,
kind="four_command",
)
cross_can1 = pair_pool[:15]
cross_can0 = tuple(item[0] for item in can0_atoms[0:8])
for index, (can1_id, can0_id) in enumerate(
((left, right) for left in cross_can1 for right in cross_can0),
start=1,
):
combine(
f"COMBO-CROSS-CHANNEL-{index:03d}",
(can1_id, can0_id),
kind="cross_can0_can1",
)
scenes = (
(
"MOVIE",
(
"CAN1-AC-ON",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-FAN-2",
"CAN1-SHADE-POSITION-100",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-3",
),
),
(
"DRIVER-COMFORT",
(
"CAN1-SEAT-DRIVER-VENT-LOW",
"CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX",
"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-FAN-2",
),
),
(
"PASSENGER-COMFORT",
(
"CAN0-SEAT-PASSENGER-HEAT-2",
"CAN0-SEAT-PASSENGER-HORIZONTAL-30",
"CAN0-SEAT-PASSENGER-BASIN-40",
"CAN0-SEAT-PASSENGER-BACKREST-120",
"CAN0-SEAT-PASSENGER-LEGREST-125",
),
),
(
"CABIN-BRIGHT",
(
"CAN1-LIGHT-INDOOR-ON",
"CAN1-LIGHT-READING-ALL-ON",
"CAN1-LIGHT-AMBIENT-ON",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-5",
),
),
(
"VENTILATION",
(
"CAN1-AC-ON",
"CAN1-AC-FAN-7",
"CAN1-AC-MODE-FACE-FEET",
"CAN1-AC-CYCLE-EXTERNAL",
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
"CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75",
),
),
(
"PARK-CLOSE",
(
"CAN1-WINDOW-ALL-CLOSE",
"CAN1-SHADE-CLOSE",
"CAN1-DOOR-REAR-LOCK",
"CAN1-LIGHT-INDOOR-OFF",
"CAN1-LIGHT-AMBIENT-OFF",
"CAN1-AC-OFF",
),
),
(
"DUAL-SEAT",
(
"CAN1-SEAT-DRIVER-VENT-HIGH",
"CAN1-SEAT-DRIVER-MASSAGE-MODE-WAVE",
"CAN0-SEAT-PASSENGER-VENT-2",
"CAN0-SEAT-PASSENGER-BACKREST-120",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-TEMP-PASSENGER-24",
),
),
(
"ALL-ZONES-CLIMATE",
(
"CAN1-AC-ON",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-TEMP-PASSENGER-24",
"CAN1-AC-TEMP-REAR-24",
"CAN1-AC-FAN-4",
"CAN1-AC-CONTROL-AUTO",
"CAN1-AC-CYCLE-INTERNAL",
),
),
(
"WINDOWS-SHADE",
(
"CAN1-WINDOW-FRONT-LEFT-POSITION-25",
"CAN1-WINDOW-FRONT-RIGHT-POSITION-50",
"CAN1-WINDOW-REAR-LEFT-POSITION-100",
"CAN1-WINDOW-REAR-RIGHT-POSITION-0",
"CAN1-SHADE-POSITION-30",
),
),
(
"DRIVER-POSTURE",
(
"CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD",
"CAN1-SEAT-DRIVER-HEIGHT-UP",
"CAN1-SEAT-DRIVER-BACKREST-BACKWARD",
"CAN1-SEAT-DRIVER-BASIN-DOWN",
),
),
)
for scene_id, source_ids in scenes:
combine(
f"COMBO-SCENE-{scene_id}",
source_ids,
kind="multi_command_scene",
)
safety_cases = (
("DRIVER-VENT-LEVEL-4", "主驾座椅通风4档"),
("DRIVER-VENT-MIDDLE", "主驾座椅通风中档"),
("DRIVER-ABSOLUTE-BACKREST", "把驾驶位靠背调到50%"),
("DRIVER-UNVERIFIED-MODE", "主驾进入零重力模式"),
("FRONT-ROW-SEAT", "打开前排座椅加热"),
("PASSENGER-HEAT-LEVEL-4", "副驾座椅加热开到4档"),
("PASSENGER-VENT-MISSING-STATE", "副驾座椅通风"),
("PASSENGER-CONTRADICTORY", "打开副驾加热然后关闭副驾加热"),
("PASSENGER-MASSAGE", "把副驾座椅按摩打开"),
("MULTIPLE-SEAT-MODES", "进入办公模式和零重力模式"),
("PASSENGER-LEGREST-EXTEND", "把副驾腿托伸长到50%"),
("WRONG-AXIS-UNIT", "坐垫向前移动10度"),
("MISSING-SEAT-AXIS", "向前移动10度"),
("REAR-SEAT", "打开后排座椅加热"),
("ALL-SEATS", "打开所有座椅加热"),
("OUTLET-DISABLE-WITHOUT-MODE", "关闭吹面出风口"),
)
for case_id, utterance in safety_cases:
add(
f"SAFETY-{case_id}",
utterance,
domains=("safety",),
channels=(),
kind="safety_rejection",
status="needs_confirmation",
)
return cases
def write_outputs(cases: list[dict[str, Any]]) -> None:
kind_counts = Counter(case["kind"] for case in cases)
channel_counts = Counter(
"+".join(case["channels"]) or "none" for case in cases
)
payload = {
"schema": "natural-can-command-combination-suite/v1",
"generated_on": "2026-08-24",
"description": "自然语言控制指令到CAN0/CAN1语义控制指令的原子、组合和安全测试目录。",
"summary": {
"total_cases": len(cases),
"by_kind": dict(sorted(kind_counts.items())),
"by_channel": dict(sorted(channel_counts.items())),
},
"cases": cases,
}
JSON_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
with CSV_PATH.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(
handle,
fieldnames=(
"id",
"kind",
"channels",
"domains",
"utterance",
"expected_status",
"expected_commands",
"expected_can0_operations",
"source_case_ids",
),
)
writer.writeheader()
for case in cases:
writer.writerow(
{
**case,
"channels": "/".join(case["channels"]),
"domains": "/".join(case["domains"]),
"expected_commands": " | ".join(case["expected_commands"]),
"expected_can0_operations": json.dumps(
case["expected_can0_operations"], ensure_ascii=False
),
"source_case_ids": " | ".join(
case.get("source_case_ids", [])
),
}
)
kind_lines = "\n".join(
f"| `{kind}` | {count} |" for kind, count in sorted(kind_counts.items())
)
channel_lines = "\n".join(
f"| `{channel}` | {count} |"
for channel, count in sorted(channel_counts.items())
)
DOC_PATH.write_text(
f"""# 自然语言转 CAN 控制指令组合测试说明
- 生成日期:2026-08-24
- 总用例数:**{len(cases)}**
- 执行方式:本地规则、`dry_run`,不发送真实 CAN 帧
- 完整逐条清单:`docs/testing/自然语言转CAN组合测试用例.csv`
- 机器可执行数据:`tests/fixtures/natural_can_command_combination_suite.json`
## 覆盖数量
| 用例类型 | 数量 |
| --- | ---: |
{kind_lines}
| 通道 | 数量 |
| --- | ---: |
{channel_lines}
## 组合策略
1. 原子用例覆盖 CAN1 门锁、车窗、空调、风口、主驾座椅、灯光、遮阳帘,以及 CAN0 副驾座椅。
2. 两两组合从 25 个互不冲突的控制槽中取组合,同时覆盖中文逗号和分号分隔形式。
3. 三指令和四指令组合覆盖 CAN0/CAN1、座椅、空调、灯光、车窗、门锁、风口与遮阳帘的交叉。
4. 跨通道组合明确检查副驾座椅留在 CAN0、主驾和车身控制留在 CAN1。
5. 多指令场景覆盖观影、双座椅、全温区空调、泊车关闭、车窗与遮阳帘等常用场景。
6. 安全用例要求白名单外目标、档位、能力和矛盾指令不生成 CAN 控制流。
## 执行
```bash
PYTHONPATH=src:domains/seat/src .venv/bin/python -m pytest -q tests/integration/test_natural_can_command_combinations.py
```
重新生成清单:
```bash
.venv/bin/python tools/generate_natural_can_command_suite.py
```
""",
encoding="utf-8",
)
def main() -> None:
cases = build_cases()
ids = [case["id"] for case in cases]
if len(ids) != len(set(ids)):
duplicates = [item for item, count in Counter(ids).items() if count > 1]
raise ValueError(f"duplicate case ids: {duplicates}")
write_outputs(cases)
print(f"generated {len(cases)} cases")
print(JSON_PATH)
print(CSV_PATH)
print(DOC_PATH)
if __name__ == "__main__":
main()