"""Safety and protocol checks for the Turtle2 action bridge.""" import importlib.util from pathlib import Path import numpy as np import pytest _BRIDGE_PATH = Path(__file__).parents[1] / "scripts" / "tcp_ws_bridge.py" _SPEC = importlib.util.spec_from_file_location("tcp_ws_bridge", _BRIDGE_PATH) bridge = importlib.util.module_from_spec(_SPEC) assert _SPEC.loader is not None _SPEC.loader.exec_module(bridge) def test_packet_limit_scales_chunk_without_cumulative_ramp(): current = np.zeros(7) traj = np.array( [ [0.10, 0.04, -0.02, 0.20, -0.10, 0.05, 0.0], [0.20, 0.08, -0.04, 0.40, -0.20, 0.10, 0.1], [0.40, 0.16, -0.08, 0.80, -0.40, 0.20, 0.2], ] ) limited, scales = bridge._limit_action_packet(traj, current, 0.005, 0.0125) np.testing.assert_allclose(limited[:, :3], traj[:, :3] * 0.005 / 0.40) np.testing.assert_allclose(limited[:, 3:6], traj[:, 3:6] * 0.0125 / 0.80) assert np.max(np.abs(limited[:, :3] - current[:3])) <= 0.005 + 1e-12 assert np.max(np.abs(limited[:, 3:6] - current[3:6])) <= 0.0125 + 1e-12 assert np.max(np.abs(np.diff(limited[:, :3], axis=0))) <= 0.005 + 1e-12 assert np.max(np.abs(np.diff(limited[:, 3:6], axis=0))) <= 0.0125 + 1e-12 assert scales["position"][1] < 1.0 assert scales["rotation"][1] < 1.0 def test_prepare_robot_actions_keeps_packet_endpoint_small(): traj = np.array( [ [0.10, 0.00, 0.00, 0.20, 0.00, 0.00, 0.0], [0.20, 0.00, 0.00, 0.40, 0.00, 0.00, 0.0], [0.30, 0.00, 0.00, 0.60, 0.00, 0.00, 0.0], [0.40, 0.00, 0.00, 0.80, 0.00, 0.00, 0.0], ] ) state = np.zeros(7) actions = bridge.prepare_robot_actions( {"follow1_pos": traj.tolist(), "follow2_pos": traj.tolist()}, state_follow1_pos=state, state_follow2_pos=state, state_head_pos=[0.0, -1.0], state_lift=[0.4], state_car_pose=np.zeros(3), action_horizon=4, action_end_ratio=1.0, action_interpolate_multiplier=1, max_position_delta=0.005, max_rotation_delta=0.0125, clip_action_delta=True, ) right = np.asarray(actions["follow2_pos"]) assert right[-1, 0] <= 0.005 + 1e-12 assert right[-1, 3] <= 0.0125 + 1e-12 assert right[-1, 0] < 0.01 # old cumulative clipping reached 4 * 0.005 def test_disabled_base_motion_sends_relative_zero(): actions = bridge.prepare_robot_actions( {"follow1_pos": [[0.0] * 7] * 2, "follow2_pos": [[0.0] * 7] * 2}, state_follow1_pos=[0.0] * 7, state_follow2_pos=[0.0] * 7, state_head_pos=[0.0, -1.0], state_lift=[0.1], state_car_pose=[1.2, -0.3, 0.7], action_horizon=2, action_end_ratio=1.0, action_interpolate_multiplier=1, max_position_delta=0.005, max_rotation_delta=0.0125, clip_action_delta=True, ) assert actions["car_pose"] == [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] def test_right_arm_only_mode_holds_serializer_left_arm(): state_left = np.array([1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.4]) right = np.array( [ [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.7], [0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.8], ] ) synthetic_left = np.full((2, 7), 99.0) actions = bridge.prepare_robot_actions( {"follow1_pos": synthetic_left.tolist(), "follow2_pos": right.tolist()}, state_follow1_pos=state_left, state_follow2_pos=np.zeros(7), state_head_pos=[0.0, -1.0], state_lift=[0.4], state_car_pose=np.zeros(3), action_horizon=2, action_end_ratio=1.0, action_interpolate_multiplier=1, ) np.testing.assert_allclose(actions["follow1_pos"], np.repeat(state_left[None, :], 2, axis=0)) np.testing.assert_allclose(actions["follow2_pos"], right) def test_right_arm_feedback_reports_missing_motion(): initial = np.zeros(7) command = np.array([0.003, 0.0, 0.0, 0.0, 0.01, 0.0, 0.5]) missing = bridge.assess_right_arm_feedback(initial, initial, command) assert missing["command_requests_motion"] assert missing["missing_feedback"] observed = np.array([0.001, 0.0, 0.0, 0.0, 0.003, 0.0, 0.1]) moved = bridge.assess_right_arm_feedback(initial, observed, command) assert moved["observed_motion"] assert not moved["missing_feedback"] def test_raw_action_response_is_rejected_before_robot_packet(): """The unsupported raw path must not reinterpret virtual padding as pose.""" raw = np.zeros((2, 26)) raw[:, :10] = [0.01, 0.02, 0.03, 1, 0, 0, 0, 1, 0, 0.8] raw[:, 16:26] = [0.20, 0.10, 0.05, 1, 0, 0, 0, 1, 0, 0.2] with pytest.raises(ValueError, match="--serialize-actions"): bridge.prepare_robot_actions( {"predict_action": raw.tolist()}, state_follow1_pos=np.zeros(7), state_follow2_pos=np.zeros(7), state_head_pos=[0, -1], state_lift=[0.1], state_car_pose=np.zeros(3), action_horizon=2, action_end_ratio=1, action_interpolate_multiplier=1, )