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
@@ -31,6 +31,13 @@ APPLY_PRESET_CAPABILITY = "seat.apply_posture_preset"
DYNAMIC_MODE_CAPABILITY = "seat.start_dynamic_mode"
DYNAMIC_MODE_STOP_CAPABILITY = "seat.stop_dynamic_mode"
DYNAMIC_MODE_MAX_RUNTIME_MS = 600_000
TIMED_MOTION_MIN_DURATION_MS = 300
TIMED_AXIS_LIMITS = {
"longitudinal": {"max_duration_ms": 30_000, "ms_per_pct": 120},
"cushion_tilt": {"max_duration_ms": 20_000, "ms_per_pct": 80},
"backrest_recline": {"max_duration_ms": 30_000, "ms_per_pct": 120},
"legrest_height": {"max_duration_ms": 25_000, "ms_per_pct": 100},
}
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")
NATIVE_CAN_ONLY_CAPABILITIES = {
"climate.set_control_mode",
@@ -556,6 +563,16 @@ class ModelPlanAdapter:
capability = request["capability"]
if capability in SEAT_MOTION_CAPABILITIES:
axis = SEAT_MOTION_CAPABILITIES[capability]
if request.get("duration_ms") is not None:
return ModelPlanAdapter._timed_motion_action(
index,
request,
ambiguities,
context,
target,
axis,
vehicle_profile,
)
physical_action = ModelPlanAdapter._relative_physical_motion_action(
index,
request,
@@ -682,6 +699,123 @@ class ModelPlanAdapter:
),
}
@staticmethod
def _timed_motion_action(
index: int,
request: dict[str, Any],
ambiguities: list[dict[str, str]],
context: dict[str, Any],
target: str,
axis: str,
vehicle_profile: VehicleProfile | None,
) -> dict[str, Any] | None:
duration_ms = request.get("duration_ms")
if (
not isinstance(duration_ms, (int, float))
or isinstance(duration_ms, bool)
or not float(duration_ms).is_integer()
or float(duration_ms) < 0
):
ambiguities.append(
{
"field": f"requests[{index}].duration_ms",
"question": "座椅按时长移动需要非负的整数毫秒数。",
}
)
return None
direction = str(request.get("operation", "set"))
if direction not in {"increase", "decrease"}:
ambiguities.append(
{
"field": f"requests[{index}].operation",
"question": "座椅按时长移动需要明确 increase 或 decrease 方向。",
}
)
return None
timing = TIMED_AXIS_LIMITS.get(axis)
if timing is None:
ambiguities.append(
{
"field": f"requests[{index}].capability",
"question": "该座椅轴当前不支持按时长运动。",
}
)
return None
previous = (
context.get("devices", {})
.get(target, {})
.get("position", {})
.get(axis)
)
if not isinstance(previous, dict):
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "缺少当前座椅位置,无法安全生成按时长运动命令。",
}
)
return None
current_pct = previous.get("position_pct")
if (
not isinstance(current_pct, (int, float))
or isinstance(current_pct, bool)
or not 0 <= float(current_pct) <= 100
):
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "当前座椅位置无效,无法安全执行按时长运动。",
}
)
return None
requested_duration_ms = int(duration_ms)
maximum_duration_ms = int(timing["max_duration_ms"])
applied_duration_ms = min(
maximum_duration_ms,
max(TIMED_MOTION_MIN_DURATION_MS, requested_duration_ms),
)
estimated_delta_pct = applied_duration_ms / int(timing["ms_per_pct"])
signed_delta = estimated_delta_pct if direction == "increase" else -estimated_delta_pct
raw_target = float(current_pct) + signed_delta
target_pct = round(max(0.0, min(100.0, raw_target)))
motion_intent: dict[str, Any] = {
"mode": "timed",
"direction": direction,
"start_position_pct": round(float(current_pct)),
"delta_position_pct": abs(target_pct - round(float(current_pct))),
"requested_duration_ms": requested_duration_ms,
"applied_duration_ms": applied_duration_ms,
"max_duration_ms": maximum_duration_ms,
"min_duration_ms": TIMED_MOTION_MIN_DURATION_MS,
"duration_clamped": applied_duration_ms != requested_duration_ms,
}
if raw_target >= 100:
motion_intent["endpoint_expected"] = "far"
elif raw_target <= 0:
motion_intent["endpoint_expected"] = "near"
action: dict[str, Any] = {
"action_type": "position",
"target": target,
"axis": axis,
"position_pct": target_pct,
"motion_intent": motion_intent,
"user_notice": f"座椅轴将按时长执行{applied_duration_ms / 1000:g}秒。",
}
motor = (
vehicle_profile.seat_motors.get(axis)
if vehicle_profile is not None
else None
)
if motor is not None and motor.display_angle is not None:
angle = motor.display_angle_for_pct(target_pct)
if angle is not None:
action["angle_deg"] = _clean_number(float(angle))
return action
@staticmethod
def _relative_physical_motion_action(
index: int,