Skip to content

Commit 5097dbe

Browse files
committed
WIP add subscriber
1 parent a6e3cea commit 5097dbe

File tree

3 files changed

+150
-17
lines changed

3 files changed

+150
-17
lines changed

artefacts.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
params:
4242
launch/world: ["bookstore.sdf"]
4343
amcl/update_min_a: [0.2]
44-
amcl/update_min_d: [0.25, 0.05]
44+
#amcl/update_min_d: [0.25, 0.05]
4545
settings:
4646
- name: reach_goal
4747
ros_testfile: "src/sam_bot_nav2_gz/test/test_reach_goal.launch.py"

src/sam_bot_nav2_gz/test/gazebo.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import subprocess
2+
import re
3+
import math
4+
import time
5+
import threading
6+
7+
from typing import Dict, Optional
8+
from xml.etree import ElementTree as ET
9+
import sys
10+
sys.path = sys.path + ['/usr/lib/python3/dist-packages']
11+
from mcap_protobuf.writer import Writer
12+
from gz.msgs10.pose_v_pb2 import Pose_V
13+
from gz.transport13 import Node
14+
15+
16+
class Pose:
17+
def __init__(self, name: str, position: Dict[str, float], orientation: Dict[str, float]):
18+
self.name = name
19+
self.position = position
20+
self.orientation = orientation
21+
22+
def distance_to(self, other: "Pose") -> float:
23+
dx = self.position.get("x", 0) - other.position.get("x", 0)
24+
dy = self.position.get("y", 0) - other.position.get("y", 0)
25+
dz = self.position.get("z", 0) - other.position.get("z", 0)
26+
return math.sqrt(dx*dx + dy*dy + dz*dz)
27+
28+
def is_near(self, other: "Pose", threshold: float = 0.5) -> bool:
29+
return self.distance_to(other) < threshold
30+
31+
32+
33+
class Gazebo:
34+
def __init__(self, recording_path: str = None):
35+
self.poses: Dict[str, Pose] = {}
36+
self.node = Node()
37+
world_name = "collision_test"
38+
self.topic_dynamicposes = f"/world/{world_name}/dynamic_pose/info"
39+
self._recording = False
40+
self._mcap_writer = None
41+
self._mcap_file = None
42+
if recording_path is not None:
43+
self._mcap_file = open(recording_path, "wb")
44+
self._mcap_writer = Writer(self._mcap_file)
45+
self._recording = True
46+
self.node.subscribe(Pose_V, self.topic_dynamicposes, self.posev_cb)
47+
48+
def update_pose(self, name: str, position: Dict[str, float], orientation: Dict[str, float]):
49+
self.poses[name] = Pose(name, position, orientation)
50+
51+
def get(self, name: str) -> Optional[Pose]:
52+
return self.poses.get(name)
53+
54+
def posev_cb(self, msg: Pose_V):
55+
if self._recording and self._mcap_writer:
56+
now = int(time.time() * 1e9)
57+
self._mcap_writer.write_message(
58+
topic=self.topic_dynamicposes,
59+
message=msg,
60+
log_time=now,
61+
publish_time=now, # do we compute from msg.header.stamp instead?, or even a sim time topic?
62+
)
63+
print("wrote 1 message")
64+
for pose_msg in msg.pose:
65+
name = pose_msg.name
66+
position = {
67+
"x": getattr(pose_msg.position, "x", 0.0),
68+
"y": getattr(pose_msg.position, "y", 0.0),
69+
"z": getattr(pose_msg.position, "z", 0.0),
70+
}
71+
orientation = {
72+
"x": getattr(pose_msg.orientation, "x", 0.0),
73+
"y": getattr(pose_msg.orientation, "y", 0.0),
74+
"z": getattr(pose_msg.orientation, "z", 0.0),
75+
"w": getattr(pose_msg.orientation, "w", 1.0),
76+
}
77+
self.update_pose(name, position, orientation)
78+
79+
def start(self):
80+
self._running = True
81+
self._thread = threading.Thread(target=self._spin_loop, daemon=True)
82+
self._thread.start()
83+
84+
def stop(self):
85+
self._running = False
86+
if hasattr(self, "_thread"):
87+
self._thread.join()
88+
89+
if self._recording:
90+
if self._mcap_file:
91+
self._mcap_writer.finish()
92+
self._mcap_file.close()
93+
self._recording = False
94+
95+
def _spin_loop(self):
96+
while self._running:
97+
time.sleep(0.001)
98+
99+
def main():
100+
if len(sys.argv) != 2:
101+
print("Usage: python recorder.py <output.mcap>")
102+
sys.exit(1)
103+
104+
output_path = sys.argv[1]
105+
gz = Gazebo(recording_path=output_path)
106+
gz.start()
107+
print(f"Recording simulation to {output_path}. Press Ctrl+C to stop.")
108+
109+
try:
110+
while True:
111+
time.sleep(1)
112+
except KeyboardInterrupt:
113+
print("Stopping recording...")
114+
gz.stop()
115+
116+
if __name__ == "__main__":
117+
main()

src/sam_bot_nav2_gz/test/test_reach_goal.launch.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414
from artefacts_toolkit.chart import make_chart
1515
from artefacts_toolkit.config import get_artefacts_param
1616

17+
# show python sys path
18+
import sys
19+
print("Python sys path:", sys.path)
20+
# add current directory to path
21+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
22+
print("Python sys path:", sys.path)
23+
from gazebo import Gazebo
24+
25+
1726

1827
# This function specifies the processes to be run for our test
1928
@pytest.mark.launch_test
@@ -54,18 +63,18 @@ def generate_test_description():
5463
)
5564

5665
# Gazebo ros bridge
57-
gz_bridge = Node(
58-
package="ros_gz_bridge",
59-
executable="parameter_bridge",
60-
parameters=[{
61-
"config_file": os.path.join(
62-
"src",
63-
"sam_bot_nav2_gz",
64-
"test",
65-
"bridge.yaml"
66-
)}],
67-
output="screen",
68-
)
66+
#gz_bridge = Node(
67+
# package="ros_gz_bridge",
68+
# executable="parameter_bridge",
69+
# parameters=[{
70+
# "config_file": os.path.join(
71+
# "src",
72+
# "sam_bot_nav2_gz",
73+
# "test",
74+
# "bridge.yaml"
75+
# )}],
76+
# output="screen",
77+
# )
6978

7079
test_odometry_node = ExecuteProcess(
7180
cmd=[
@@ -78,6 +87,8 @@ def generate_test_description():
7887
),
7988
]
8089
)
90+
gz_interface = Gazebo(recording_path="recording.mcap")
91+
gz_interface.start()
8192

8293
return LaunchDescription(
8394
[
@@ -89,11 +100,11 @@ def generate_test_description():
89100
launch_navigation_stack,
90101
reach_goal,
91102
test_odometry_node,
92-
gz_bridge,
103+
#gz_bridge,
93104
bag_recorder,
94105
ReadyToTest(),
95106
]
96-
), { "rosbag_filepath": rosbag_filepath}
107+
), { "rosbag_filepath": rosbag_filepath, "gz_interface": gz_interface}
97108

98109

99110
# This is our test fixture. Each method is a test case.
@@ -106,13 +117,14 @@ def test_nav2_started(self, proc_output):
106117
# replace the exception message with a more informative one
107118
raise AssertionError("Nav2 apparently failed to start") from e
108119

109-
def test_reached_goal(self, proc_output):
120+
def test_reached_goal(self, proc_output, gz_interface):
110121
"""Check the logs to see if the navigation task is completed"""
111122
# 'proc_output' is an object added automatically by the launch_testing framework.
112123
# It captures the outputs of the processes launched in generate_test_description()
113124
# Refer to the documentation for further details.
114125
try:
115126
proc_output.assertWaitFor("Goal succeeded!", timeout=240, stream="stdout")
127+
assert gz_interface.get("sam_bot") > 0
116128
except AssertionError as e:
117129
# replace the exception message with a more informative one
118130
raise AssertionError("Goal was not reached") from e
@@ -121,7 +133,7 @@ def test_reached_goal(self, proc_output):
121133

122134
@launch_testing.post_shutdown_test()
123135
class TestProcOutputAfterShutdown(unittest.TestCase):
124-
def test_exit_code(self, rosbag_filepath):
136+
def test_exit_code(self, rosbag_filepath, gz_interface):
125137
print(rosbag_filepath)
126138
make_chart(
127139
rosbag_filepath,
@@ -130,5 +142,9 @@ def test_exit_code(self, rosbag_filepath):
130142
field_unit="m",
131143
chart_name="odometry_position",
132144
)
145+
final_pose = gz_interface.get("sam_bot")
146+
print(final_pose)
147+
assert final_pose.position["x"] > 10000.0
148+
gz_interface.stop()
133149
image_topics.extract_camera_image(rosbag_filepath, "/sky_cam")
134150
image_topics.extract_video(rosbag_filepath, "/sky_cam", "output/sky_cam.webm")

0 commit comments

Comments
 (0)