mirror of
https://github.com/introlab/rtabmap_ros.git
synced 2026-10-06 09:47:46 +08:00
fix(rtabmap_slam): release scan mutex on conversion failure
This commit is contained in:
@@ -1801,6 +1801,7 @@ void CoreWrapper::commonLaserScanCallback(
|
||||
rtabmap_.getMemory() && uStrNumCmp(rtabmap_.getMemory()->getDatabaseVersion(), "0.11.10") < 0))
|
||||
{
|
||||
RCLCPP_ERROR(this->get_logger(), "Could not convert laser scan msg! Aborting rtabmap update...");
|
||||
syncDataMutex_.unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1819,6 +1820,7 @@ void CoreWrapper::commonLaserScanCallback(
|
||||
scanCloudIs2d_))
|
||||
{
|
||||
RCLCPP_ERROR(this->get_logger(), "Could not convert 3d laser scan msg! Aborting rtabmap update...");
|
||||
syncDataMutex_.unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Scan transform recovery regression
|
||||
|
||||
`scan_tf_recovery.py` is a standalone ROS 2 integration reproducer for the
|
||||
scan-conversion error paths in `CoreWrapper::commonLaserScanCallback`.
|
||||
It is not registered with CTest yet.
|
||||
|
||||
Source your ROS installation and the workspace containing the RTAB-Map build
|
||||
under test. Use an unused ROS domain and run the two cases sequentially:
|
||||
|
||||
```bash
|
||||
source /opt/ros/jazzy/setup.bash
|
||||
source install/setup.bash
|
||||
export ROS_DOMAIN_ID=171
|
||||
python3 src/rtabmap_ros/rtabmap_slam/test/scan_tf_recovery.py
|
||||
python3 src/rtabmap_ros/rtabmap_slam/test/scan_tf_recovery.py --late-tf
|
||||
```
|
||||
|
||||
Adjust the script path for your checkout layout. Python dependencies are
|
||||
`rclpy`, `ament_index_python`, `geometry_msgs`, `nav_msgs`, `sensor_msgs`,
|
||||
`sensor_msgs_py`, `std_msgs`, `tf2_ros` and PyYAML.
|
||||
|
||||
The script starts the installed `rtabmap_slam/rtabmap` executable, publishes
|
||||
odometry and `odom -> base_link`, and begins publishing PointCloud2 data after
|
||||
four seconds. The control provides `base_link -> lidar` immediately; `--late-tf`
|
||||
withholds it until eight seconds. No simulator, hardware or recorded data is
|
||||
required. Parameters and captured logs are written under `/tmp`.
|
||||
|
||||
Both cases require a nonempty occupancy grid within 18 seconds and a clean
|
||||
process exit after SIGINT. The late-TF case also requires a scan-conversion error
|
||||
in the log, ensuring the failure path was exercised. A JSON summary reports the
|
||||
map dimensions, conversion failure and process exit code.
|
||||
|
||||
On stock Jazzy 0.23.7, the control passes but the late-TF case produces no map and
|
||||
requires forced shutdown. With the two missing unlocks added, both pass. The
|
||||
patched late-TF case passed five runs, including two using default FastDDS and
|
||||
two using UDP-only transport. This reproducer exercises the 3D scan path; the
|
||||
analogous 2D error path is fixed by inspection but is not independently tested.
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Probe first-map recovery when the scan's static transform arrives late."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import rclpy
|
||||
import yaml
|
||||
from ament_index_python.packages import get_package_prefix
|
||||
from geometry_msgs.msg import TransformStamped
|
||||
from nav_msgs.msg import OccupancyGrid, Odometry
|
||||
from rclpy.qos import DurabilityPolicy, QoSProfile, ReliabilityPolicy
|
||||
from sensor_msgs.msg import PointCloud2
|
||||
from sensor_msgs_py import point_cloud2
|
||||
from std_msgs.msg import Header
|
||||
from tf2_ros import StaticTransformBroadcaster, TransformBroadcaster
|
||||
|
||||
|
||||
def run(late_tf: bool) -> int:
|
||||
rclpy.init()
|
||||
node = rclpy.create_node("startup_input_probe")
|
||||
best = QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT)
|
||||
cloud_pub = node.create_publisher(PointCloud2, "/scan_cloud", best)
|
||||
odom_pub = node.create_publisher(Odometry, "/odom", 10)
|
||||
static = StaticTransformBroadcaster(node)
|
||||
dynamic = TransformBroadcaster(node)
|
||||
received = []
|
||||
|
||||
def on_map(msg: OccupancyGrid) -> None:
|
||||
if msg.info.width and msg.info.height:
|
||||
received.append([msg.info.width, msg.info.height])
|
||||
|
||||
node.create_subscription(
|
||||
OccupancyGrid,
|
||||
"/map",
|
||||
on_map,
|
||||
QoSProfile(depth=1, durability=DurabilityPolicy.TRANSIENT_LOCAL),
|
||||
)
|
||||
parameters = {
|
||||
"frame_id": "base_link",
|
||||
"odom_frame_id": "odom",
|
||||
"map_frame_id": "map",
|
||||
"subscribe_depth": "false",
|
||||
"subscribe_rgb": "false",
|
||||
"subscribe_scan_cloud": "true",
|
||||
"subscribe_odom": "true",
|
||||
"approx_sync": "true",
|
||||
"qos_scan": "2",
|
||||
"qos_odom": "1",
|
||||
"wait_for_transform": "0.2",
|
||||
"database_path": "",
|
||||
"Reg/Strategy": "1",
|
||||
"Grid/3D": "true",
|
||||
"Grid/CellSize": "0.1",
|
||||
"Grid/NormalsSegmentation": "false",
|
||||
"Grid/MinGroundHeight": "-0.3",
|
||||
"Grid/MaxGroundHeight": "0.1",
|
||||
"Grid/RangeMax": "20.0",
|
||||
"Grid/RangeMin": "0.1",
|
||||
}
|
||||
args = [
|
||||
str(Path(get_package_prefix("rtabmap_slam")) / "lib/rtabmap_slam/rtabmap"),
|
||||
"--ros-args",
|
||||
"--log-level",
|
||||
"warn",
|
||||
]
|
||||
parameters = {
|
||||
key: (value if "/" in key or key == "database_path" else yaml.safe_load(value))
|
||||
for key, value in parameters.items()
|
||||
}
|
||||
Path("/tmp/rtab-startup-params.yaml").write_text(
|
||||
yaml.safe_dump({"/**": {"ros__parameters": parameters}})
|
||||
)
|
||||
args += ["--params-file", "/tmp/rtab-startup-params.yaml"]
|
||||
log_path = Path("/tmp/rtab-startup-probe.log")
|
||||
with log_path.open("w") as log:
|
||||
process = subprocess.Popen(
|
||||
args, stdout=log, stderr=subprocess.STDOUT, start_new_session=True
|
||||
)
|
||||
start = time.monotonic()
|
||||
static_sent = False
|
||||
cloud_sent = False
|
||||
try:
|
||||
while time.monotonic() - start < 18 and not received:
|
||||
elapsed = time.monotonic() - start
|
||||
stamp = node.get_clock().now().to_msg()
|
||||
transform = TransformStamped()
|
||||
transform.header.stamp = stamp
|
||||
transform.header.frame_id = "odom"
|
||||
transform.child_frame_id = "base_link"
|
||||
transform.transform.rotation.w = 1.0
|
||||
dynamic.sendTransform(transform)
|
||||
odom = Odometry()
|
||||
odom.header.stamp = stamp
|
||||
odom.header.frame_id = "odom"
|
||||
odom.child_frame_id = "base_link"
|
||||
odom.pose.pose.orientation.w = 1.0
|
||||
for i in [0, 7, 14, 21, 28, 35]:
|
||||
odom.pose.covariance[i] = 0.001
|
||||
odom_pub.publish(odom)
|
||||
if not static_sent and (not late_tf or elapsed > 8):
|
||||
transform.header.frame_id = "base_link"
|
||||
transform.child_frame_id = "lidar"
|
||||
transform.transform.translation.z = 0.2
|
||||
static.sendTransform(transform)
|
||||
static_sent = True
|
||||
if elapsed > 4 and cloud_pub.get_subscription_count() > 0:
|
||||
header = Header(stamp=stamp, frame_id="lidar")
|
||||
points = [
|
||||
(x / 10.0, y / 10.0, -0.2)
|
||||
for x in range(5, 41, 2)
|
||||
for y in range(-20, 21, 2)
|
||||
]
|
||||
points += [
|
||||
(4.0, y / 10.0, z / 10.0)
|
||||
for y in range(-20, 21, 2)
|
||||
for z in range(0, 21, 2)
|
||||
]
|
||||
cloud_pub.publish(point_cloud2.create_cloud_xyz32(header, points))
|
||||
cloud_sent = True
|
||||
rclpy.spin_once(node, timeout_sec=0.05)
|
||||
if process.poll() is not None:
|
||||
break
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
os.killpg(process.pid, signal.SIGINT)
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait()
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
||||
content = log_path.read_text()
|
||||
result = {
|
||||
"lateTf": late_tf,
|
||||
"cloudSent": cloud_sent,
|
||||
"map": received[:1],
|
||||
"conversionFailure": "Could not convert 3d laser scan msg" in content,
|
||||
"processExit": process.returncode,
|
||||
}
|
||||
print(json.dumps(result), flush=True)
|
||||
passed = (
|
||||
bool(received)
|
||||
and process.returncode == 0
|
||||
and (not late_tf or result["conversionFailure"])
|
||||
)
|
||||
if not passed:
|
||||
print(content[-5000:], flush=True)
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--late-tf", action="store_true")
|
||||
args = parser.parse_args()
|
||||
raise SystemExit(run(args.late_tf))
|
||||
Reference in New Issue
Block a user