Unify navigation on NavigateToPose and remove legacy proxies

This commit is contained in:
2026-09-22 17:40:25 +08:00
parent 964d1fde67
commit 24e0b922bc
40 changed files with 461 additions and 2592 deletions
+56 -51
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generated Navigate messages + real DDS + production C++ RosDriver regression.
"""Generated NavigateToPose messages + real DDS + production C++ RosDriver regression.
In a sourced ROS Humble overlay:
python3 tests/helpers/native_navigation_contract.py --build-native --output /tmp/nav-contract
@@ -30,13 +30,14 @@ find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(bt_skill_interfaces REQUIRED)
find_package(navigation_interfaces REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(nlohmann_json REQUIRED)
add_executable(native_navigation_probe %s %s %s)
target_include_directories(native_navigation_probe PRIVATE %s %s)
target_link_libraries(native_navigation_probe nlohmann_json::nlohmann_json)
ament_target_dependencies(native_navigation_probe rclcpp rclcpp_action bt_skill_interfaces geometry_msgs std_msgs)
ament_target_dependencies(native_navigation_probe rclcpp rclcpp_action bt_skill_interfaces navigation_interfaces geometry_msgs std_msgs)
''' % tuple(q(root / p) for p in (
'ros2/bt_executor/tools/native_navigation_probe.cpp',
'ros2/bt_executor/src/ros_driver.cpp', 'core/src/core.cpp',
@@ -60,21 +61,23 @@ def run(binary, output):
from rclpy.action import ActionClient, ActionServer, CancelResponse
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.executors import MultiThreadedExecutor
from bt_skill_interfaces.action import Navigate
from bt_skill_interfaces.msg import NavigationResult
from navigation_interfaces.action import NavigateToPose as Navigate
from action_msgs.msg import GoalStatus
assert [getattr(NavigationResult, name) for name in
assert [getattr(Navigate.Result, name) for name in
('SUCCEEDED', 'CANCELED', 'TIMEOUT', 'BLOCKED', 'NOT_READY', 'FAILED')] == list(range(6))
assert [getattr(Navigate.Feedback, name) for name in
('ACCEPTED', 'CHECKING', 'NAVIGATING', 'BLOCKED', 'STOPPING')] == list(range(5))
('ACCEPTED', 'CHECKING', 'PLANNING', 'NAVIGATING', 'BLOCKED', 'STOPPING')] == list(range(6))
assert set(Navigate.Goal.get_fields_and_field_types()) == {
'trace', 'target_pose', 'position_tolerance', 'yaw_tolerance', 'timeout'}
'task_id', 'subtask_id', 'target_pose', 'position_tolerance', 'yaw_tolerance', 'timeout'}
assert set(Navigate.Result.get_fields_and_field_types()) == {
'result', 'final_pose_valid', 'final_pose', 'final_position_error', 'final_yaw_error'}
'status', 'error_code', 'message', 'final_pose_valid', 'final_pose',
'final_position_error', 'final_yaw_error', 'stop_state', 'stopped_at', 'stop_evidence_ref'}
assert set(Navigate.Feedback.get_fields_and_field_types()) == {
'stamp', 'sequence', 'phase', 'current_pose_valid', 'current_pose', 'error_valid',
'position_error', 'yaw_error', 'blocked', 'elapsed_time', 'message'}
'position_error', 'yaw_error', 'blocked_valid', 'blocked', 'elapsed_time', 'message'}
assert Navigate.Feedback.get_fields_and_field_types()['sequence'] == 'uint64'
assert (Navigate.Result.STOP_UNKNOWN, Navigate.Result.STOP_CONFIRMED) == (0, 1)
namespace = '/sim/navigation_contract_' + str(os.getpid())
rclpy.init()
node = Node('navigation_contract_fixture', namespace=namespace)
@@ -85,51 +88,51 @@ def run(binary, output):
def execute(handle):
goal = handle.request
scenario = round(goal.target_pose.pose.position.x)
status = scenario if scenario < 6 else {6: 1, 7: 3, 8: 4}[scenario]
status = scenario if scenario < 6 else {6: 1, 7: 3, 8: 4, 9: 1, 10: 4}[scenario]
with lock:
counts[(goal.trace.task_id, scenario)] += 1
counts[(goal.task_id, scenario)] += 1
try:
assert goal.target_pose.header.frame_id == 'map'
assert goal.target_pose.pose.orientation.w == 1.
assert goal.position_tolerance == .05 and goal.yaw_tolerance == .1
assert goal.timeout.sec == 5
assert goal.trace.attempt == 1 and goal.trace.task_revision == 1
assert goal.trace.plan_version == 1 and goal.trace.execution_generation == 1
assert goal.task_id and goal.subtask_id == 'navigate'
except AssertionError:
failures.append('Goal contract mismatch: ' + str(goal))
for phase in range(5):
for phase in range(6):
feedback = Navigate.Feedback()
feedback.stamp = node.get_clock().now().to_msg()
feedback.sequence = phase + 1
feedback.sequence = (1 << 32) + phase + 1
feedback.phase = phase
feedback.current_pose_valid = True
feedback.current_pose = goal.target_pose
feedback.error_valid = True
feedback.position_error = .02
feedback.yaw_error = -.03
feedback.blocked_valid = phase != Navigate.Feedback.STOPPING
feedback.blocked = phase == Navigate.Feedback.BLOCKED
feedback.elapsed_time.nanosec = (phase + 1) * 10000000
feedback.message = 'phase-' + str(phase)
handle.publish_feedback(feedback)
time.sleep(.03)
if phase == 2 and goal.trace.task_id.startswith('probe-'):
if phase == 2 and goal.task_id.startswith('probe-'):
# A later malformed sample must not poison the accepted sequence.
wrong_frame = deepcopy(feedback)
wrong_frame.sequence = 100
wrong_frame.sequence = (1 << 32) + 100
wrong_frame.current_pose.header.frame_id = 'odom'
handle.publish_feedback(wrong_frame)
wrong_phase = deepcopy(feedback)
wrong_phase.sequence = 101
wrong_phase.sequence = (1 << 32) + 101
wrong_phase.phase = 255
handle.publish_feedback(wrong_phase)
result = Navigate.Result()
result.result.status = status
result.result.error_code = '' if scenario in (7, 8) else 'FIXTURE_' + str(scenario)
result.result.message = 'outcome-' + str(scenario)
result.result.stop_state = 0 if scenario == 6 else 1
result.result.stopped_at = node.get_clock().now().to_msg()
result.result.stop_evidence_ref = '' if scenario == 6 else 'sim://navigation/stop'
result.final_pose_valid = True
result.status = status
result.error_code = '' if scenario in (7, 8) else 'EXECUTION_BACKEND_NOT_CONFIGURED' if scenario == 10 else 'FIXTURE_' + str(scenario)
result.message = 'outcome-' + str(scenario)
result.stop_state = 0 if scenario in (6, 10) else 1
result.stopped_at = node.get_clock().now().to_msg()
result.stop_evidence_ref = '' if scenario in (6, 9, 10) else 'sim://navigation/stop'
result.final_pose_valid = scenario != 10
result.final_pose = goal.target_pose
result.final_position_error = .02
result.final_yaw_error = -.03
@@ -156,16 +159,14 @@ def run(binary, output):
executor.add_node(node)
thread = threading.Thread(target=executor.spin, daemon=True)
thread.start()
report = {'scope': 'simulation-only generated Navigate and production RosDriver over DDS',
report = {'scope': 'simulation-only generated NavigateToPose and production RosDriver over DDS',
'namespace': namespace, 'direct': [], 'native': []}
try:
assert client.wait_for_server(timeout_sec=10)
for scenario in range(9):
for scenario in range(11):
goal = Navigate.Goal()
goal.trace.task_id = 'direct-' + str(scenario)
goal.trace.run_id = goal.trace.task_id
goal.trace.subtask_id = 'navigate'
goal.trace.attempt = goal.trace.task_revision = goal.trace.plan_version = goal.trace.execution_generation = 1
goal.task_id = 'direct-' + str(scenario)
goal.subtask_id = 'navigate'
goal.target_pose.header.frame_id = 'map'
goal.target_pose.pose.position.x = float(scenario)
goal.target_pose.pose.orientation.w = 1.
@@ -175,25 +176,26 @@ def run(binary, output):
feedback = []
handle = wait(client.send_goal_async(goal, feedback_callback=lambda value: feedback.append(value.feedback)))
assert handle.accepted
if scenario in (1, 6):
if scenario in (1, 6, 9):
assert wait(handle.cancel_goal_async()).return_code == 0
response = wait(handle.get_result_async())
expected = scenario if scenario < 6 else {6: 1, 7: 3, 8: 4}[scenario]
assert response.result.result.status == expected
expected = scenario if scenario < 6 else {6: 1, 7: 3, 8: 4, 9: 1, 10: 4}[scenario]
assert response.result.status == expected
assert response.status == (GoalStatus.STATUS_SUCCEEDED if expected == 0 else
GoalStatus.STATUS_CANCELED if expected == 1 else GoalStatus.STATUS_ABORTED)
assert response.result.final_pose_valid
assert response.result.final_pose_valid == (scenario != 10)
assert response.result.final_pose.pose.position.x == float(scenario)
assert response.result.final_position_error == .02 and response.result.final_yaw_error == -.03
assert response.result.result.stop_state == (0 if scenario == 6 else 1)
assert response.result.result.error_code == ('' if scenario in (7, 8) else 'FIXTURE_' + str(scenario))
assert [f.phase for f in feedback] == list(range(5)), feedback
assert [f.sequence for f in feedback] == list(range(1, 6))
assert response.result.stop_state == (0 if scenario in (6, 10) else 1)
assert response.result.error_code == ('' if scenario in (7, 8) else 'EXECUTION_BACKEND_NOT_CONFIGURED' if scenario == 10 else 'FIXTURE_' + str(scenario))
assert [f.phase for f in feedback] == list(range(6)), feedback
assert [f.sequence for f in feedback] == [(1 << 32) + n for n in range(1, 7)]
assert all(f.current_pose_valid and f.error_valid and f.position_error == .02 and
f.yaw_error == -.03 and f.stamp.sec > 0 for f in feedback)
assert feedback[3].blocked and not feedback[4].blocked
assert feedback[4].blocked_valid and feedback[4].blocked
assert not feedback[5].blocked_valid and not feedback[5].blocked
report['direct'].append({'scenario': scenario, 'status': expected, 'feedback_phases': [f.phase for f in feedback]})
for scenario, expected_code in enumerate((0, 2, 3, 1, 4, 1, 2, 1, 4)):
for scenario, expected_code in enumerate((0, 2, 3, 1, 4, 1, 2, 1, 4, 2, 4)):
state = output / ('state-' + str(scenario))
state.mkdir(exist_ok=True)
completed = subprocess.run([str(binary), str(scenario), str(state), namespace],
@@ -208,19 +210,22 @@ def run(binary, output):
raise AssertionError('Native probe failed: ' + json.dumps(report['failure']))
result = json.loads(next(line for line in reversed(completed.stdout.splitlines()) if line.startswith('{')))
assert result['code'] == expected_code, result
assert result['stop'] == (0 if scenario == 6 else 1), result
assert result['state'] == (3 if scenario == 6 else 4), result
assert result['robot_locked'] == (scenario == 6), result
assert result['unknown_stop_blocks_redispatch'] == (scenario == 6), result
assert result['error_code'] == ({7: 'NAV_BLOCKED', 8: 'NAV_NOT_READY'}.get(scenario, 'FIXTURE_' + str(scenario))), result
assert result['feedback_sequence'] == 5, result
assert json.loads(result['feedback'])['phase'] == 4, result
assert result['stop'] == (0 if scenario in (6, 9, 10) else 1), result
assert result['state'] == (3 if scenario in (6, 9, 10) else 4), result
assert result['robot_locked'] == (scenario in (6, 9, 10)), result
assert result['unknown_stop_blocks_redispatch'] == (scenario in (6, 9, 10)), result
assert result['error_code'] == ({7: 'NAV_BLOCKED', 8: 'NAV_NOT_READY', 10: 'EXECUTION_BACKEND_NOT_CONFIGURED'}.get(scenario, 'FIXTURE_' + str(scenario))), result
assert result['feedback_sequence'] == (1 << 32) + 6, result
assert json.loads(result['feedback'])['phase'] == 5
assert json.loads(result['feedback'])['blocked_valid'] is False, result
assert json.loads(result['feedback'])['blocked'] is None, result
assert result['mapping_count'] == 1 and result['wire_result_bytes'] > 0, result
assert 'Navigate' in result['wire_request_type'] and 'Navigate' in result['wire_result_type'], result
assert result['response_valid'], result
assert result['wire_request_type'] == 'navigation_interfaces/action/NavigateToPose_Goal', result
assert result['wire_result_type'] == 'navigation_interfaces/action/NavigateToPose_Result', result
assert result['response_valid'] == (scenario != 10), result
report['native'].append(result)
assert not failures, failures
assert len(counts) == 18 and all(count == 1 for count in counts.values()), dict(counts)
assert len(counts) == 22 and all(count == 1 for count in counts.values()), dict(counts)
report['exactly_once_goal_count'] = sum(counts.values())
report['passed'] = True
(output / 'navigation-contract-report.json').write_text(json.dumps(report, indent=2) + '\n')