优化了副驾座椅控制
This commit is contained in:
@@ -378,6 +378,9 @@ class HardwareSeatExecutor:
|
||||
elif frame.arbitration_id == 0x4FA and len(frame.data) >= 8:
|
||||
values = self.matrix.decode_by_frame_id(0x4FA, frame.data)
|
||||
self._record_thermal_state(state, values, frame)
|
||||
elif frame.arbitration_id == 0x306 and len(frame.data) >= 8:
|
||||
values = self.matrix.decode_by_frame_id(0x306, frame.data)
|
||||
self._record_dynamic_state(state, values, frame)
|
||||
state["captured_at"] = _now()
|
||||
state["source"] = "physical_can"
|
||||
state["target"] = {
|
||||
@@ -461,6 +464,30 @@ class HardwareSeatExecutor:
|
||||
self._set_thermal_feature(state, feature, raw)
|
||||
state[feature]["feedback_frame"] = frame.public()
|
||||
|
||||
@staticmethod
|
||||
def _record_dynamic_state(
|
||||
state: dict[str, Any],
|
||||
values: dict[str, int],
|
||||
frame: CanFrame,
|
||||
) -> None:
|
||||
mode_id = int(values["Dynamic_Mode_ID"])
|
||||
dynamic_state = int(values["Dynamic_State"])
|
||||
names = {1: "木马模式", 2: "摇篮模式"}
|
||||
states = {0: "idle", 1: "entry", 2: "running", 3: "fault"}
|
||||
state["dynamic_mode"] = {
|
||||
"mode_id": mode_id,
|
||||
"name": names.get(mode_id, ""),
|
||||
"state": states.get(dynamic_state, "unknown"),
|
||||
"phase": int(values["Dynamic_Phase"]),
|
||||
"active_mask": int(values["Dynamic_Active_Mask"]),
|
||||
"fault_mask": int(values["Dynamic_Fault_Mask"]),
|
||||
"cycle_index": int(values["Dynamic_Cycle_Index"]),
|
||||
"phase_elapsed_ms": int(values["Dynamic_Phase_Elapsed_Ms"]),
|
||||
"repeat": "bounded_runtime" if dynamic_state in {1, 2} else "none",
|
||||
"max_runtime_ms": 600_000,
|
||||
"feedback_frame": frame.public(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _set_thermal_feature(
|
||||
state: dict[str, Any], feature: str, raw: int
|
||||
|
||||
@@ -20,28 +20,55 @@ STATE_PATTERN = re.compile(
|
||||
)
|
||||
LEVEL_PATTERN = re.compile(r"(最高|最低|[1-5一二三四五高中低])\s*档")
|
||||
POSITION_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:位置)?(?:调到|移动到|移到|设为|设置为|调至|移动至|到|为)?"
|
||||
r"{alias}(?:位置)?(?:调到|调至|移动到|移动至|移到|移至|"
|
||||
r"转动到|转动至|旋转到|旋转至|调节到|调节至|调整到|调整至|"
|
||||
r"设为|设置为|设置到|设置至|设置|设|到|为)?"
|
||||
r"(?P<position>[+-]?\d{{1,3}}(?:\.\d+)?)%"
|
||||
)
|
||||
ANGLE_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:夹角|角度)?(?:调到|放倒到|抬到|移动到|移到|设为|设置为|"
|
||||
r"调至|到|为)?(?P<angle>\d{{2,3}})(?:度|°)"
|
||||
r"{alias}(?:夹角|角度)?(?:调到|调至|放倒到|抬到|移动到|移动至|移到|移至|"
|
||||
r"转动到|转动至|旋转到|旋转至|调节到|调节至|调整到|调整至|"
|
||||
r"设为|设置为|设置到|设置至|设置|设|到|为)?"
|
||||
r"(?P<angle>\d{{2,3}})(?:度|°)"
|
||||
)
|
||||
RELATIVE_ANGLE_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:再|继续|接着|然后)?(?P<direction>向前|往前|朝前|前倾|向后|往后|朝后|后仰|"
|
||||
r"{alias}(?:再|继续|接着|然后)?"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|设置|设|倾斜|抬|升|降)?"
|
||||
r"(?:再|继续|接着|然后)?"
|
||||
r"(?P<direction>向前|往前|朝前|前倾|向后|往后|朝后|后仰|"
|
||||
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下|前|后|上|下)"
|
||||
r"(?:移动|移|调节|调整|调|倾斜|抬|升|降)?(?P<delta>\d{{1,3}})(?:度|°)"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|设置|设|倾斜|抬|升|降)?"
|
||||
r"(?P<delta>\d{{1,3}})(?:度|°)"
|
||||
)
|
||||
RELATIVE_PERCENT_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:再|继续|接着|然后)?(?P<direction>向前|往前|朝前|前移|向后|往后|朝后|后移|"
|
||||
r"{alias}(?:再|继续|接着|然后)?"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|设置|设|倾斜|抬|升|降)?"
|
||||
r"(?:再|继续|接着|然后)?"
|
||||
r"(?P<direction>向前|往前|朝前|前移|向后|往后|朝后|后移|"
|
||||
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下|前|后|上|下)"
|
||||
r"(?:移动|移|调节|调整|调|倾斜|抬|升|降)?(?P<delta>\d{{1,3}})%"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|设置|设|倾斜|抬|升|降)?"
|
||||
r"(?P<delta>\d{{1,3}})%"
|
||||
)
|
||||
DIRECTIONAL_ABSOLUTE_PERCENT_PATTERN = re.compile(
|
||||
r"(?P<direction>向前|往前|朝前|前移|向后|往后|朝后|后移|"
|
||||
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下)"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|设置|设|倾斜|抬|升|降)*"
|
||||
r"(?:到|至|为)"
|
||||
r"(?P<position>[+-]?\d{1,3}(?:\.\d+)?)%"
|
||||
)
|
||||
DIRECTIONAL_RELATIVE_PERCENT_PATTERN = re.compile(
|
||||
r"(?P<direction>向前|往前|朝前|前移|向后|往后|朝后|后移|"
|
||||
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下)"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|设置|设|倾斜|抬|升|降)?"
|
||||
r"(?P<delta>\d{1,3})%"
|
||||
)
|
||||
TIMED_MOTION_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:持续|保持)?(?:再|继续|接着|然后)?"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|倾斜|抬|升|降)?"
|
||||
r"(?:再|继续|接着|然后)?"
|
||||
r"(?P<direction>向前|往前|朝前|前移|前倾|向后|往后|朝后|后移|后仰|"
|
||||
r"向上|往上|抬高|抬起|升高|向下|往下|降低|降下|前|后|上|下)"
|
||||
r"(?:移动|移|调节|调整|调|倾斜|抬|升|降|运行|运动|持续|保持|时长)*"
|
||||
r"(?:移动|移|转动|旋转|转|调节|调整|调|倾斜|抬|升|降|运行|运动|持续|保持|时长)*"
|
||||
r"(?P<duration>\d+(?:\.\d+)?)(?P<unit>毫秒|ms|秒|s|分钟|分|min)"
|
||||
)
|
||||
ZERO_POSITION_PATTERN = re.compile(r"归零|回零|复位|零位")
|
||||
@@ -126,7 +153,14 @@ MOTION_AXIS_ALIASES = {
|
||||
"longitudinal": ("水平", "前后", "纵向", "滑轨"),
|
||||
# Internal capability name is retained for compatibility. On the
|
||||
# seat-ota CAN0 firmware this board axis is the physical seat-height axis.
|
||||
"cushion_tilt": ("座椅高度", "高度", "坐垫倾角", "坐盆", "坐垫"),
|
||||
"cushion_tilt": (
|
||||
"座椅高度",
|
||||
"高度",
|
||||
"坐垫倾角",
|
||||
"坐盆倾角",
|
||||
"坐盆",
|
||||
"坐垫",
|
||||
),
|
||||
"backrest_recline": ("靠背", "椅背"),
|
||||
"legrest_height": ("腿部支撑", "小腿托", "腿托", "脚托"),
|
||||
}
|
||||
@@ -222,6 +256,7 @@ def _normalize_text(value: str) -> str:
|
||||
text = re.sub(r"座椅(?:吹风|送风)", "座椅通风", text)
|
||||
text = re.sub(r"座椅(?:取暖|加温)", "座椅加热", text)
|
||||
text = text.replace("座椅暖一下", "座椅加热打开")
|
||||
text = text.replace("推托", "腿托")
|
||||
|
||||
def replace_percent(match: re.Match[str]) -> str:
|
||||
number = _chinese_integer(match.group(1))
|
||||
@@ -251,6 +286,74 @@ def _relative_direction(axis: str, token: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _directional_motion_axis(clause: str, direction: str) -> str | None:
|
||||
"""Resolve an omitted passenger-seat axis from the movement direction."""
|
||||
|
||||
explicit_axes = [
|
||||
axis
|
||||
for axis, aliases in MOTION_AXIS_ALIASES.items()
|
||||
if any(alias in clause for alias in aliases)
|
||||
]
|
||||
if len(explicit_axes) == 1:
|
||||
return explicit_axes[0]
|
||||
if explicit_axes:
|
||||
return None
|
||||
if TARGET_PATTERN.search(clause) is None and not re.search(r"座椅|座位", clause):
|
||||
return None
|
||||
if direction in {
|
||||
"向前",
|
||||
"往前",
|
||||
"朝前",
|
||||
"前移",
|
||||
"向后",
|
||||
"往后",
|
||||
"朝后",
|
||||
"后移",
|
||||
}:
|
||||
return "longitudinal"
|
||||
if direction in {
|
||||
"向上",
|
||||
"往上",
|
||||
"抬高",
|
||||
"抬起",
|
||||
"升高",
|
||||
"向下",
|
||||
"往下",
|
||||
"降低",
|
||||
"降下",
|
||||
}:
|
||||
return "cushion_tilt"
|
||||
return None
|
||||
|
||||
|
||||
def _absolute_percent_motion_action(
|
||||
axis: str, requested_position_value: float
|
||||
) -> dict[str, Any]:
|
||||
requested_position_pct = _round_half_away_from_zero(requested_position_value)
|
||||
position_pct = max(0, min(100, requested_position_pct))
|
||||
action: dict[str, Any] = {
|
||||
"target": "passenger",
|
||||
"action_type": "position",
|
||||
"axis": axis,
|
||||
"position_pct": position_pct,
|
||||
}
|
||||
notices: list[str] = []
|
||||
if requested_position_value != requested_position_pct:
|
||||
notices.append(
|
||||
f"{MOTION_AXIS_LABELS[axis]}请求位置"
|
||||
f"{_format_numeric_request(requested_position_value)}%;"
|
||||
f"已取整为{requested_position_pct}%。"
|
||||
)
|
||||
if requested_position_pct != position_pct:
|
||||
notices.append(
|
||||
f"取整后超过0到100%行程;已采用{position_pct}%的备用目标"
|
||||
"并移动到对应机械末端。"
|
||||
)
|
||||
if notices:
|
||||
action["user_notice"] = " ".join(notices)
|
||||
return action
|
||||
|
||||
|
||||
def _context_motion_state(
|
||||
context: dict[str, Any] | None, axis: str
|
||||
) -> dict[str, Any] | None:
|
||||
@@ -982,6 +1085,32 @@ def parse_local_intent(
|
||||
if shared_position is not None:
|
||||
continue
|
||||
|
||||
directional_absolute_axes: set[str] = set()
|
||||
directional_absolute_match = DIRECTIONAL_ABSOLUTE_PERCENT_PATTERN.search(clause)
|
||||
if directional_absolute_match is not None:
|
||||
axis = _directional_motion_axis(
|
||||
clause, directional_absolute_match.group("direction")
|
||||
)
|
||||
if axis is not None:
|
||||
directional_absolute_axes.add(axis)
|
||||
target_zh = (
|
||||
_nearest_preceding_target(
|
||||
clause, directional_absolute_match.start()
|
||||
)
|
||||
or "副驾"
|
||||
)
|
||||
if "driver" in TARGETS[target_zh]:
|
||||
ambiguities.append(
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
)
|
||||
else:
|
||||
motion_actions.append(
|
||||
_absolute_percent_motion_action(
|
||||
axis,
|
||||
float(directional_absolute_match.group("position")),
|
||||
)
|
||||
)
|
||||
|
||||
timed_axes: set[str] = set()
|
||||
timed_aliases = {
|
||||
**MOTION_AXIS_ALIASES,
|
||||
@@ -1022,7 +1151,7 @@ def parse_local_intent(
|
||||
|
||||
relative_axes: set[str] = set()
|
||||
for axis, aliases in MOTION_AXIS_ALIASES.items():
|
||||
if axis in timed_axes:
|
||||
if axis in timed_axes or axis in directional_absolute_axes:
|
||||
continue
|
||||
for alias in aliases:
|
||||
angle_match = re.search(
|
||||
@@ -1062,6 +1191,46 @@ def parse_local_intent(
|
||||
motion_actions.append(action)
|
||||
break
|
||||
|
||||
directional_relative_match = DIRECTIONAL_RELATIVE_PERCENT_PATTERN.search(clause)
|
||||
if (
|
||||
directional_relative_match is not None
|
||||
and not relative_axes
|
||||
and not directional_absolute_axes
|
||||
):
|
||||
axis = _directional_motion_axis(
|
||||
clause, directional_relative_match.group("direction")
|
||||
)
|
||||
if axis is not None and axis not in timed_axes:
|
||||
relative_axes.add(axis)
|
||||
target_zh = (
|
||||
_nearest_preceding_target(
|
||||
clause, directional_relative_match.start()
|
||||
)
|
||||
or "副驾"
|
||||
)
|
||||
if "driver" in TARGETS[target_zh]:
|
||||
ambiguities.append(
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
)
|
||||
else:
|
||||
action, question = _relative_motion_action(
|
||||
axis,
|
||||
directional_relative_match.group("direction"),
|
||||
int(directional_relative_match.group("delta")),
|
||||
"percent",
|
||||
context,
|
||||
)
|
||||
if action is None:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": f"passenger.position.{axis}",
|
||||
"question": question
|
||||
or "无法计算座椅相对移动目标。",
|
||||
}
|
||||
)
|
||||
else:
|
||||
motion_actions.append(action)
|
||||
|
||||
endpoint_actions: dict[str, int] = {}
|
||||
has_specific_non_longitudinal_axis = any(
|
||||
any(alias in clause for alias in MOTION_AXIS_ALIASES[axis])
|
||||
@@ -1078,7 +1247,7 @@ def parse_local_intent(
|
||||
endpoint_actions["longitudinal"] = 0
|
||||
elif re.search(r"最后面|最后|向后移到底|移到最后", clause):
|
||||
endpoint_actions["longitudinal"] = 100
|
||||
cushion_alias = r"(?:座椅高度|高度|坐垫倾角|坐盆|坐垫)"
|
||||
cushion_alias = r"(?:座椅高度|高度|坐垫倾角|坐盆倾角|坐盆|坐垫)"
|
||||
if re.search(
|
||||
cushion_alias + r"(?:抬到最上方|调到最上方|移到最上方|最高|抬到头|抬到底)",
|
||||
clause,
|
||||
@@ -1128,7 +1297,12 @@ def parse_local_intent(
|
||||
)
|
||||
|
||||
for axis, aliases in MOTION_AXIS_ALIASES.items():
|
||||
if axis in endpoint_actions or axis in relative_axes or axis in timed_axes:
|
||||
if (
|
||||
axis in endpoint_actions
|
||||
or axis in relative_axes
|
||||
or axis in timed_axes
|
||||
or axis in directional_absolute_axes
|
||||
):
|
||||
continue
|
||||
for alias in aliases:
|
||||
position_match = re.search(
|
||||
|
||||
@@ -74,6 +74,9 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
"把主驾和副驾温度都设为24度",
|
||||
"风量调到2档",
|
||||
"设置主驾出风模式为吹脸",
|
||||
"把副驾车窗移动到30%",
|
||||
"将主驾空调温度设置为24度",
|
||||
"把遮阳帘转动到20%",
|
||||
)
|
||||
for utterance in utterances:
|
||||
with self.subTest(utterance=utterance):
|
||||
@@ -159,6 +162,128 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
self.assertEqual(result["actions"][0]["position_pct"], 65)
|
||||
self.assertEqual(result["actions"][0]["motion_intent"]["direction"], "decrease")
|
||||
|
||||
def test_parses_passenger_directional_relative_percentages(self) -> None:
|
||||
context = self.motion_context()
|
||||
for state in context["devices"]["passenger"]["position"].values():
|
||||
state["position_pct"] = 50
|
||||
cases = (
|
||||
("副驾座椅往上移动10%", "cushion_tilt", "decrease", 10),
|
||||
("副驾座椅往上移动20%", "cushion_tilt", "decrease", 20),
|
||||
("副驾往下移动10%", "cushion_tilt", "increase", 10),
|
||||
("副驾往下移动20%", "cushion_tilt", "increase", 20),
|
||||
("副驾座椅往前移动10%", "longitudinal", "decrease", 10),
|
||||
("副驾座椅往前移动20%", "longitudinal", "decrease", 20),
|
||||
("副驾往后移动10%", "longitudinal", "increase", 10),
|
||||
("副驾往后移动20%", "longitudinal", "increase", 20),
|
||||
("副驾座椅靠背往前移动10%", "backrest_recline", "decrease", 10),
|
||||
("副驾座椅靠背往前移动30%", "backrest_recline", "decrease", 30),
|
||||
("副驾靠背往后移动10%", "backrest_recline", "increase", 10),
|
||||
("副驾靠背往后移动30%", "backrest_recline", "increase", 30),
|
||||
("副驾推托往上移动20%", "legrest_height", "increase", 20),
|
||||
("副驾推托往上移动40%", "legrest_height", "increase", 40),
|
||||
("副驾推托往下移动20%", "legrest_height", "decrease", 20),
|
||||
("副驾推托往下移动40%", "legrest_height", "decrease", 40),
|
||||
)
|
||||
|
||||
for utterance, axis, direction, delta in cases:
|
||||
with self.subTest(utterance=utterance):
|
||||
result = parse_local_intent(utterance, context)
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertEqual(len(result["actions"]), 1)
|
||||
action = result["actions"][0]
|
||||
self.assertEqual(action["axis"], axis)
|
||||
self.assertEqual(action["motion_intent"]["mode"], "relative")
|
||||
self.assertEqual(action["motion_intent"]["direction"], direction)
|
||||
self.assertEqual(action["motion_intent"]["delta_position_pct"], delta)
|
||||
|
||||
def test_motion_verbs_are_weak_signals_for_relative_seat_controls(self) -> None:
|
||||
context = self.motion_context()
|
||||
for state in context["devices"]["passenger"]["position"].values():
|
||||
state["position_pct"] = 50
|
||||
cases = (
|
||||
("副驾座椅往前移动10%", "longitudinal", "decrease", 10),
|
||||
("副驾靠背往后转动20%", "backrest_recline", "increase", 20),
|
||||
("副驾腿托往上调节30%", "legrest_height", "increase", 30),
|
||||
("副驾座椅高度往下设置10%", "cushion_tilt", "increase", 10),
|
||||
("副驾靠背转动往前20%", "backrest_recline", "decrease", 20),
|
||||
("副驾腿托设置往下30%", "legrest_height", "decrease", 30),
|
||||
("副驾靠背往后旋转10度", "backrest_recline", "increase", 10),
|
||||
)
|
||||
|
||||
for utterance, axis, direction, amount in cases:
|
||||
with self.subTest(utterance=utterance):
|
||||
result = parse_local_intent(utterance, context)
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertEqual(len(result["actions"]), 1)
|
||||
action = result["actions"][0]
|
||||
self.assertEqual(action["axis"], axis)
|
||||
self.assertEqual(action["motion_intent"]["mode"], "relative")
|
||||
self.assertEqual(action["motion_intent"]["direction"], direction)
|
||||
motion = action["motion_intent"]
|
||||
requested = motion.get("requested_delta_angle_deg")
|
||||
if requested is None:
|
||||
requested = motion.get("requested_delta_position_pct")
|
||||
if requested is None:
|
||||
requested = motion.get(
|
||||
"delta_angle_deg", motion.get("delta_position_pct")
|
||||
)
|
||||
self.assertEqual(requested, amount)
|
||||
|
||||
def test_directional_move_to_percentage_is_absolute_not_relative(self) -> None:
|
||||
cases = (
|
||||
("副驾座椅往上移动到10%", "cushion_tilt", 10),
|
||||
("副驾座椅往上移动到20%", "cushion_tilt", 20),
|
||||
("副驾往下移动到10%", "cushion_tilt", 10),
|
||||
("副驾往下移动到20%", "cushion_tilt", 20),
|
||||
("副驾座椅往前移动到10%", "longitudinal", 10),
|
||||
("副驾座椅往前移动到20%", "longitudinal", 20),
|
||||
("副驾往后移动到10%", "longitudinal", 10),
|
||||
("副驾往后移动到20%", "longitudinal", 20),
|
||||
("副驾座椅靠背往前移动到10%", "backrest_recline", 10),
|
||||
("副驾座椅靠背往前移动到30%", "backrest_recline", 30),
|
||||
("副驾靠背往后移动到10%", "backrest_recline", 10),
|
||||
("副驾靠背往后移动到30%", "backrest_recline", 30),
|
||||
("副驾推托往上移动到20%", "legrest_height", 20),
|
||||
("副驾推托往上移动到40%", "legrest_height", 40),
|
||||
("副驾推托往下移动到20%", "legrest_height", 20),
|
||||
("副驾推托往下移动到40%", "legrest_height", 40),
|
||||
)
|
||||
|
||||
for utterance, axis, position in cases:
|
||||
with self.subTest(utterance=utterance):
|
||||
result = parse_local_intent(utterance, self.motion_context())
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertEqual(len(result["actions"]), 1)
|
||||
action = result["actions"][0]
|
||||
self.assertEqual(action["axis"], axis)
|
||||
self.assertEqual(action["position_pct"], position)
|
||||
self.assertNotIn("motion_intent", action)
|
||||
|
||||
def test_motion_verbs_with_position_markers_are_absolute(self) -> None:
|
||||
cases = (
|
||||
("副驾滑轨移动至60%", "longitudinal", 60),
|
||||
("副驾靠背转动到30%", "backrest_recline", 30),
|
||||
("副驾腿托调节至40%", "legrest_height", 40),
|
||||
("副驾座椅高度设置为20%", "cushion_tilt", 20),
|
||||
("副驾靠背往后转动到35%", "backrest_recline", 35),
|
||||
("副驾腿托往上设置为45%", "legrest_height", 45),
|
||||
("副驾靠背旋转到130度", "backrest_recline", 51),
|
||||
)
|
||||
|
||||
for utterance, axis, position in cases:
|
||||
with self.subTest(utterance=utterance):
|
||||
result = parse_local_intent(utterance, self.motion_context())
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertEqual(len(result["actions"]), 1)
|
||||
action = result["actions"][0]
|
||||
self.assertEqual(action["axis"], axis)
|
||||
self.assertEqual(action["position_pct"], position)
|
||||
self.assertNotIn("motion_intent", action)
|
||||
|
||||
def test_rejects_relative_motion_without_state_and_clamps_endpoint_overflow(self) -> None:
|
||||
missing = parse_local_intent("靠背向前移动10度")
|
||||
context = self.motion_context()
|
||||
|
||||
Reference in New Issue
Block a user