diff --git a/src/iiwa_experement/iiwa_experement/runtime_config.py b/src/iiwa_experement/iiwa_experement/runtime_config.py index 58beec9..41d9307 100644 --- a/src/iiwa_experement/iiwa_experement/runtime_config.py +++ b/src/iiwa_experement/iiwa_experement/runtime_config.py @@ -17,7 +17,7 @@ import yaml EXPERIMENT_IDS = tuple(range(1, 21)) -MODES = ('view', 'execute') +MODES = ('view', 'execute', 'planner') DEFAULT_START_DELAY_SEC = 3.0 DEFAULT_TRACE_DIR = '~/.ros/iiwa_experement/runs' LOCAL_PROTOS = ( diff --git a/src/iiwa_experement/launch/experiment.launch.py b/src/iiwa_experement/launch/experiment.launch.py index 4c2f24a..f24a197 100644 --- a/src/iiwa_experement/launch/experiment.launch.py +++ b/src/iiwa_experement/launch/experiment.launch.py @@ -110,6 +110,11 @@ def supervisor_process( ) +def mode_read_only(mode: str) -> bool: + """Only ``view`` is passive; execute and planner accept grab commands.""" + return mode == 'view' + + def runtime_actions( experiment_id: int, scene_config: str, @@ -126,7 +131,10 @@ def runtime_actions( package_share = Path(get_package_share_directory('iiwa_experement')) if geometry_path is None: geometry_path = str(package_share / 'config' / 'geometry.yaml') - read_only = mode != 'execute' + # Only the passive view mode locks the supervisor. Both the reference + # executor and the external-graph planner need to accept grab/dock + # commands; the planner mode simply never starts the reference runner. + read_only = mode_read_only(mode) actions = [ IncludeLaunchDescription( PythonLaunchDescriptionSource( @@ -264,7 +272,9 @@ def generate_launch_description(): default_value='view', description=( 'view = open the prepared scene without motion; ' - 'execute = run the reference sequence after start_delay_sec.' + 'execute = run the reference sequence after start_delay_sec; ' + 'planner = writable scene for an externally supplied graph, ' + 'no reference sequence.' ), ), DeclareLaunchArgument( diff --git a/src/iiwa_experement/test/test_launch.py b/src/iiwa_experement/test/test_launch.py index 6449958..e428a7c 100644 --- a/src/iiwa_experement/test/test_launch.py +++ b/src/iiwa_experement/test/test_launch.py @@ -45,12 +45,17 @@ def test_invalid_experiment_ids_are_rejected(raw): validate_experiment_id(raw) -@pytest.mark.parametrize('raw, expected', [('view', 'view'), ('execute', 'execute'), (' VIEW ', 'view')]) +@pytest.mark.parametrize('raw, expected', [ + ('view', 'view'), + ('execute', 'execute'), + ('planner', 'planner'), + (' VIEW ', 'view'), +]) def test_valid_modes(raw, expected): assert validate_mode(raw) == expected -@pytest.mark.parametrize('raw', ['', 'run', 'VIEW '[:-1] + 'x', '1']) +@pytest.mark.parametrize('raw', ['', 'run', 'VIEW '[:-1] + 'x', '1', 'plan']) def test_invalid_modes_are_rejected(raw): with pytest.raises(ValueError): validate_mode(raw) @@ -138,6 +143,46 @@ def test_execute_adds_the_reference_runner_after_the_supervisor(): assert any(type(action) is ExecuteProcess for action in actions) +def _process_arguments(process): + return [item[0].text for item in process.cmd if item and item[0].text] + + +def test_planner_is_writable_and_has_no_reference_runner(): + module = load_launch_module() + actions = module.runtime_actions( + experiment_id=20, + scene_config='/tmp/scenes.yaml', + setting_path='/tmp/setting.yaml', + mode='planner', + ) + assert not any( + isinstance(action, Node) and action.node_executable == 'reference_runner' + for action in actions + ) + processes = [action for action in actions if type(action) is ExecuteProcess] + assert len(processes) == 1 + assert 'read_only:=false' in _process_arguments(processes[0]) + assert module.mode_read_only('planner') is False + parameters = module.supervisor_parameters( + 20, '/tmp/scenes.yaml', '/tmp/geometry.yaml', False, + ) + assert parameters['read_only'] is False + + +def test_view_remains_read_only_after_adding_the_planner_mode(): + module = load_launch_module() + actions = module.runtime_actions( + experiment_id=1, + scene_config='/tmp/scenes.yaml', + setting_path='/tmp/setting.yaml', + mode='view', + ) + processes = [action for action in actions if type(action) is ExecuteProcess] + assert 'read_only:=true' in _process_arguments(processes[0]) + assert module.mode_read_only('view') is True + assert module.mode_read_only('execute') is False + + def test_runtime_actions_can_register_cleanup(tmp_path): module = load_launch_module() actions = module.runtime_actions( diff --git a/src/iiwa_experement/test/test_runtime_config.py b/src/iiwa_experement/test/test_runtime_config.py index f135a3a..4c5e038 100644 --- a/src/iiwa_experement/test/test_runtime_config.py +++ b/src/iiwa_experement/test/test_runtime_config.py @@ -14,12 +14,17 @@ from iiwa_experement.runtime_config import ( ) -@pytest.mark.parametrize('raw, expected', [('view', 'view'), (' Execute ', 'execute')]) +@pytest.mark.parametrize('raw, expected', [ + ('view', 'view'), + (' Execute ', 'execute'), + ('planner', 'planner'), + (' PLANNER ', 'planner'), +]) def test_mode_validation(raw, expected): assert validate_mode(raw) == expected -@pytest.mark.parametrize('raw', ['', 'motion', 'execute!']) +@pytest.mark.parametrize('raw', ['', 'motion', 'execute!', 'plan']) def test_mode_validation_rejects_unknown_modes(raw): with pytest.raises(ValueError): validate_mode(raw)