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