feat: complete dual-seat whole-vehicle orchestration

This commit is contained in:
vee
2026-08-12 15:12:40 +08:00
parent 32379a1573
commit 9ce64b3a01
62 changed files with 9741 additions and 3950 deletions
@@ -362,7 +362,12 @@ class T2GSeatCompiler:
"Motor_Selector": int(axis["motor_selector"]),
"Opcode": int(self.protocol["timed_duty_opcode"]),
"Param_A": duration_ms,
"Param_B": int(self.protocol["default_duty_percent"])
"Param_B": int(
axis.get(
"timed_duty_percent",
self.protocol["default_duty_percent"],
)
)
* int(self.protocol["timed_duty_level_scale"]),
"Protocol_Version": int(self.protocol["version"]),
"Sequence": 0,
@@ -426,7 +431,12 @@ class T2GSeatCompiler:
"Motor_Selector": int(axis["motor_selector"]),
"Opcode": int(self.protocol["timed_duty_opcode"]),
"Param_A": duration_ms,
"Param_B": int(self.protocol["default_duty_percent"])
"Param_B": int(
axis.get(
"timed_duty_percent",
self.protocol["default_duty_percent"],
)
)
* int(self.protocol["timed_duty_level_scale"]),
"Protocol_Version": int(self.protocol["version"]),
"Sequence": 0,
+18 -17
View File
@@ -28,19 +28,20 @@ ANGLE_PATTERN_TEMPLATE = (
r"调至|到|为)?(?P<angle>\d{{2,3}})(?:度|°)"
)
RELATIVE_ANGLE_PATTERN_TEMPLATE = (
r"{alias}(?P<direction>向前|往前|朝前|前倾|向后|往后|朝后|后仰|"
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下)"
r"(?:移动|调节|调整|调|倾斜|抬|升|降)?(?P<delta>\d{{1,3}})(?:度|°)"
r"{alias}(?:再|继续|接着|然后)?(?P<direction>向前|往前|朝前|前倾|向后|往后|朝后|后仰|"
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下|前|后|上|下)"
r"(?:移动|移|调节|调整|调|倾斜|抬|升|降)?(?P<delta>\d{{1,3}})(?:度|°)"
)
RELATIVE_PERCENT_PATTERN_TEMPLATE = (
r"{alias}(?P<direction>向前|往前|朝前|前移|向后|往后|朝后|后移|"
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下)"
r"(?:移动|调节|调整|调|倾斜|抬|升|降)?(?P<delta>\d{{1,3}})%"
r"{alias}(?:再|继续|接着|然后)?(?P<direction>向前|往前|朝前|前移|向后|往后|朝后|后移|"
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下|前|后|上|下)"
r"(?:移动|移|调节|调整|调|倾斜|抬|升|降)?(?P<delta>\d{{1,3}})%"
)
TIMED_MOTION_PATTERN_TEMPLATE = (
r"{alias}(?:持续|保持)?(?P<direction>向前|往前|朝前|前移|前倾|向后|往后|朝后|后移|后仰|"
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下)"
r"(?:移动|调节|调整|调|倾斜|抬|升|降|运行|运动|持续|保持|时长)*"
r"{alias}(?:持续|保持)?(?:再|继续|接着|然后)?"
r"(?P<direction>向前|往前|朝前|前移|前倾|向后|往后|朝后|后移|后仰|"
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下|前|后|上|下)"
r"(?:移动|移|调节|调整|调|倾斜|抬|升|降|运行|运动|持续|保持|时长)*"
r"(?P<duration>\d+(?:\.\d+)?)(?P<unit>毫秒|ms|秒|s|分钟|分|min)"
)
ZERO_POSITION_PATTERN = re.compile(r"归零|回零|复位|零位")
@@ -129,20 +130,20 @@ CHINESE_DURATION_PATTERN = re.compile(
)
RELATIVE_DIRECTION_SIGNS = {
"longitudinal": {
"increase": {"向后", "往后", "朝后", "后移"},
"decrease": {"向前", "往前", "朝前", "前移"},
"increase": {"向后", "往后", "朝后", "后移", "后"},
"decrease": {"向前", "往前", "朝前", "前移", "前"},
},
"cushion_tilt": {
"increase": {"向下", "往下", "降低", "降下"},
"decrease": {"向上", "往上", "抬高", "抬起", "升高"},
"increase": {"向下", "往下", "降低", "降下", "下"},
"decrease": {"向上", "往上", "抬高", "抬起", "升高", "上"},
},
"backrest_recline": {
"increase": {"向后", "往后", "朝后", "后仰"},
"decrease": {"向前", "往前", "朝前", "前倾"},
"increase": {"向后", "往后", "朝后", "后仰", "后"},
"decrease": {"向前", "往前", "朝前", "前倾", "前"},
},
"legrest_height": {
"increase": {"向上", "往上", "抬高", "抬起", "升高"},
"decrease": {"向下", "往下", "降低", "降下"},
"increase": {"向上", "往上", "抬高", "抬起", "升高", "上"},
"decrease": {"向下", "往下", "降低", "降下", "下"},
},
}
TIMED_MOTION_MIN_DURATION_MS = 300
@@ -509,6 +509,63 @@ class SeatServiceIntegrationTests(unittest.TestCase):
108.6,
)
def test_model_timed_motion_survives_endpoint_resolution_and_reaches_compiler(self) -> None:
self.service.process(
"副驾座椅水平位置调到100%",
provider="local",
conversation_id="conv-model-timed",
)
model_output = {
"detected_domains": ["seat"],
"requests": [
{
"domain": "seat",
"capability": "seat.set_longitudinal_position",
"target_scope": "passenger",
"operation": "increase",
"duration_ms": 3_000,
"parameters": [{"name": "position_pct", "value": 100}],
"reason": "Continue moving the rail backward for three seconds.",
},
{
"domain": "seat",
"capability": "seat.set_legrest_height",
"target_scope": "passenger",
"operation": "increase",
"duration_ms": 3_000,
"parameters": [],
"reason": "Raise the leg rest for three seconds.",
},
],
"ambiguities": [],
"decision_summary": ["保留两个动作各自三秒的定时运动语义。"],
"confidence": 0.99,
}
self.service.openai.api_key = "test-key"
with patch.object(self.service.openai, "interpret", return_value=model_output):
result = self.service.process(
"副驾座椅水平再后移动3秒,副驾腿托向上调3秒",
"openai",
"conv-model-timed",
vehicle_id="denza_n7",
)
self.assertEqual(result["status"], "succeeded")
operations = result["function_config"]["spec"]["operations"]
self.assertEqual(len(operations), 2)
by_capability = {item["capability"]: item for item in operations}
rail_motion = by_capability["seat.set_longitudinal_position"]["motion"]
self.assertEqual(rail_motion["mode"], "timed")
self.assertEqual(rail_motion["direction"], "increase")
self.assertEqual(rail_motion["start_position_pct"], 100)
self.assertEqual(rail_motion["delta_position_pct"], 0)
self.assertEqual(rail_motion["applied_duration_ms"], 3_000)
legrest_motion = by_capability["seat.set_legrest_height"]["motion"]
self.assertEqual(legrest_motion["mode"], "timed")
self.assertEqual(legrest_motion["direction"], "increase")
self.assertEqual(legrest_motion["applied_duration_ms"], 3_000)
def test_vehicle_profile_angle_limits_are_accepted(self) -> None:
model_output = {
"detected_domains": ["seat"],
+20
View File
@@ -301,6 +301,26 @@ class LocalIntentParserTests(unittest.TestCase):
self.assertTrue(minimum["actions"][0]["motion_intent"]["duration_clamped"])
self.assertIn("最小允许时长为0.3秒", minimum["actions"][0]["user_notice"])
def test_timed_motion_accepts_continuation_and_compact_direction_phrases(self) -> None:
cases = {
"座椅水平再后移动3秒": ("longitudinal", "increase"),
"座椅水平往后移3秒": ("longitudinal", "increase"),
"座椅水平继续前移3秒": ("longitudinal", "decrease"),
"腿托向上调3秒": ("legrest_height", "increase"),
"腿托再向上调3秒": ("legrest_height", "increase"),
"腿托再下调3秒": ("legrest_height", "decrease"),
}
for utterance, (axis, direction) in cases.items():
with self.subTest(utterance=utterance):
result = parse_local_intent(utterance, self.motion_context())
self.assertEqual(result["ambiguities"], [])
self.assertEqual(len(result["actions"]), 1)
self.assertEqual(result["actions"][0]["axis"], axis)
motion = result["actions"][0]["motion_intent"]
self.assertEqual(motion["mode"], "timed")
self.assertEqual(motion["direction"], direction)
self.assertEqual(motion["requested_duration_ms"], 3_000)
def test_rounds_decimal_and_clamps_negative_absolute_positions(self) -> None:
decimal = parse_local_intent("副驾座椅水平位置调到50.5%")
negative = parse_local_intent("副驾座椅水平位置调到-10%")
+31 -1
View File
@@ -494,6 +494,9 @@ class ProtocolSignatureFakeBus:
)
self.received.append(CanFrame(0x305, bytes(8)))
self.received.append(CanFrame(0x306, bytes(8)))
self.received.append(
CanFrame(0x307, bytes.fromhex("53 4D 56 38 0F 10 04 01"))
)
self.received.append(CanFrame(0x4FA, bytes.fromhex("09 00 00 00 00 00 00 00")))
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
@@ -702,6 +705,32 @@ class T2GHardwareTests(unittest.TestCase):
self.assertEqual(command["signals"]["Param_B"], 900)
self.assertEqual(command["completion"]["timeout_ms"], 35_000)
def test_compiler_uses_bench_safe_legrest_timed_duty(self) -> None:
config = function_config(
operation(
"seat.set_legrest_height",
{"position_pct": 30, "angle_deg": 117},
motion={
"mode": "timed",
"direction": "increase",
"start_position_pct": 0,
"delta_position_pct": 30,
"requested_duration_ms": 3_000,
"applied_duration_ms": 3_000,
"max_duration_ms": 25_000,
"min_duration_ms": 300,
"duration_clamped": False,
},
)
)
command = self.compiler.compile(config, {})["spec"]["commands"][0]
self.assertEqual(command["signals"]["Motor_Selector"], 3)
self.assertEqual(command["signals"]["Opcode"], 3)
self.assertEqual(command["signals"]["Param_A"], 3_000)
self.assertEqual(command["signals"]["Param_B"], 500)
def test_driver_target_is_rejected_by_hardware_compiler(self) -> None:
driver = function_config(
operation(
@@ -1685,6 +1714,7 @@ class T2GHardwareTests(unittest.TestCase):
self.assertIs(prepared, bus)
self.assertFalse(telemetry["programmed_this_request"])
self.assertEqual(telemetry["firmware_revision"], 15)
self.assertEqual(telemetry["firmware_build_id"], 16)
self.assertEqual(bus.sent, [])
self.assertEqual(telemetry["max_hall_counts"], [480, 374, 917, 647])
self.assertFalse(bus.closed)
@@ -1756,7 +1786,7 @@ class T2GHardwareTests(unittest.TestCase):
message = str(raised.exception)
self.assertIn("firmware_revision=0 (expected 15)", message)
self.assertIn("missing_frame_ids=0x306", message)
self.assertIn("missing_frame_ids=0x306,0x307", message)
self.assertIn("max_hall_counts=480,374,917,647", message)
self.assertTrue(bus.closed)