feat: complete dual-seat whole-vehicle orchestration
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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%")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user