mirror of
https://github.com/introlab/rtabmap_ros.git
synced 2026-10-12 04:29:49 +08:00
113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
"""
|
|||
|
|
Ctrl-C on `ros2 launch` leaves rtabmap's database complete.
|
||
|
|
|
||
|
|
Ctrl-C sends SIGINT to the whole process group, and `ros2 launch` then sends its own
|
||
|
|
SIGINT to each node: rtabmap gets two. It saves what it kept in memory only -- the
|
||
|
|
optimized graph, the links added to nodes already in the database -- when it closes the
|
||
|
|
database, so it has to survive the second SIGINT until then.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import signal
|
||
|
|
import sqlite3
|
||
|
|
import subprocess
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import rclpy
|
||
|
|
from geometry_msgs.msg import TransformStamped
|
||
|
|
from nav_msgs.msg import Odometry
|
||
|
|
from rclpy.qos import QoSProfile
|
||
|
|
from rtabmap_msgs.msg import Info
|
||
|
|
from tf2_msgs.msg import TFMessage
|
||
|
|
|
||
|
|
LAUNCH_FILE = Path(__file__).parent / 'rtabmap_odom_only.launch.py'
|
||
|
|
NODES = 5
|
||
|
|
|
||
|
|
|
||
|
|
def _odometry(stamp: float, x: float):
|
||
|
|
"""odom -> base_link at @p x, as TF and as an odometry message, the way the C++
|
||
|
|
tests' sendOdom() sends them."""
|
||
|
|
tf = TransformStamped()
|
||
|
|
tf.header.frame_id = 'odom'
|
||
|
|
tf.header.stamp.sec = int(stamp)
|
||
|
|
tf.child_frame_id = 'base_link'
|
||
|
|
tf.transform.translation.x = x
|
||
|
|
tf.transform.rotation.w = 1.0
|
||
|
|
odom = Odometry()
|
||
|
|
odom.header = tf.header
|
||
|
|
odom.child_frame_id = 'base_link'
|
||
|
|
odom.pose.pose.position.x = x
|
||
|
|
odom.pose.pose.orientation.w = 1.0
|
||
|
|
odom.pose.covariance = [0.001 if i % 7 == 0 else 0.0 for i in range(36)]
|
||
|
|
odom.twist.covariance = odom.pose.covariance
|
||
|
|
return TFMessage(transforms=[tf]), odom
|
||
|
|
|
||
|
|
|
||
|
|
def _spin_until(node, done, timeout: float) -> bool:
|
||
|
|
end = time.monotonic() + timeout
|
||
|
|
while time.monotonic() < end:
|
||
|
|
if done():
|
||
|
|
return True
|
||
|
|
rclpy.spin_once(node, timeout_sec=0.05)
|
||
|
|
return done()
|
||
|
|
|
||
|
|
|
||
|
|
def _map(node, launch):
|
||
|
|
"""Drives NODES odometry updates into rtabmap, waiting for each to be processed."""
|
||
|
|
tf_pub = node.create_publisher(TFMessage, '/tf', QoSProfile(depth=100))
|
||
|
|
odom_pub = node.create_publisher(Odometry, 'odom', 10)
|
||
|
|
info = []
|
||
|
|
node.create_subscription(Info, 'info', info.append, 10)
|
||
|
|
started = lambda: (launch.poll() is not None or ( # noqa: E731
|
||
|
|
odom_pub.get_subscription_count() > 0 and tf_pub.get_subscription_count() > 0
|
||
|
|
and node.count_publishers('info') > 0))
|
||
|
|
assert _spin_until(node, started, 60.0) and launch.poll() is None, 'rtabmap did not start'
|
||
|
|
# Matched on this side only: rtabmap's side matches on its own time, and until then
|
||
|
|
# its best-effort subscription (Fast DDS's default) drops what it is sent, and it
|
||
|
|
# does not publish info, having no subscriber.
|
||
|
|
_spin_until(node, lambda: False, 1.0)
|
||
|
|
for i in range(NODES):
|
||
|
|
tf, odom = _odometry(1.0 + i, 0.5 * i)
|
||
|
|
tf_pub.publish(tf)
|
||
|
|
odom_pub.publish(odom)
|
||
|
|
assert _spin_until(node, lambda: len(info) > i, 10.0), f'update {i} was not processed'
|
||
|
|
|
||
|
|
|
||
|
|
def test_ctrl_c_saves_the_database(tmp_path):
|
||
|
|
database = tmp_path / 'rtabmap.db'
|
||
|
|
log_path = tmp_path / 'launch.log'
|
||
|
|
with open(log_path, 'w') as log:
|
||
|
|
launch = subprocess.Popen(
|
||
|
|
['ros2', 'launch', str(LAUNCH_FILE), f'database_path:={database}',
|
||
|
|
f'working_directory:={tmp_path}'],
|
||
|
|
stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
|
||
|
|
rclpy.init()
|
||
|
|
try:
|
||
|
|
node = rclpy.create_node('rtabmap_slam_test_shutdown')
|
||
|
|
try:
|
||
|
|
_map(node, launch)
|
||
|
|
finally:
|
||
|
|
node.destroy_node()
|
||
|
|
os.killpg(launch.pid, signal.SIGINT) # Ctrl-C
|
||
|
|
launch.wait(timeout=60)
|
||
|
|
except AssertionError as e:
|
||
|
|
raise AssertionError(f'{e}\n{log_path.read_text()}') from None
|
||
|
|
finally:
|
||
|
|
rclpy.try_shutdown()
|
||
|
|
if launch.poll() is None:
|
||
|
|
os.killpg(launch.pid, signal.SIGKILL)
|
||
|
|
launch.wait()
|
||
|
|
log = log_path.read_text()
|
||
|
|
|
||
|
|
assert 'process has finished cleanly' in log, log
|
||
|
|
with sqlite3.connect(database) as db:
|
||
|
|
optimized = db.execute('SELECT opt_poses FROM Admin').fetchone()[0]
|
||
|
|
# Each neighbor link is in both directions; new -> old goes in with the new node,
|
||
|
|
# old -> new is added to a node already saved, and is written at closing.
|
||
|
|
forward, backward = (
|
||
|
|
db.execute(f'SELECT COUNT(*) FROM Link WHERE type = 0 AND from_id {op} to_id')
|
||
|
|
.fetchone()[0] for op in ('<', '>'))
|
||
|
|
assert optimized, 'no optimized graph saved'
|
||
|
|
assert (forward, backward) == (NODES - 1, NODES - 1)
|