Skip to content

Commit 2161605

Browse files
authored
Merge pull request #18 from JdeRobot/issue-17
Added ROS2 support
2 parents b78d277 + eeeb1d5 commit 2161605

7 files changed

Lines changed: 1016 additions & 1 deletion

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import json
2+
import logging
3+
import os.path
4+
import subprocess
5+
import sys
6+
import threading
7+
import time
8+
from threading import Thread
9+
10+
from src.manager.libs.applications.compatibility.client import Client
11+
from src.manager.libs.process_utils import stop_process_and_children
12+
from src.manager.ram_logging.log_manager import LogManager
13+
from src.manager.manager.application.robotics_python_application_interface import IRoboticsPythonApplication
14+
from src.manager.manager.lint.linter import Lint
15+
16+
17+
class CompatibilityExerciseWrapperRos2(IRoboticsPythonApplication):
18+
def __init__(self, exercise_command, gui_command, update_callback):
19+
super().__init__(update_callback)
20+
21+
home_dir = os.path.expanduser('~')
22+
self.running = False
23+
self.linter = Lint()
24+
self.brain_ready_event = threading.Event()
25+
# TODO: review hardcoded values
26+
process_ready, self.exercise_server = self._run_exercise_server(f"python3 {exercise_command}",
27+
f'{home_dir}/ws_code.log',
28+
'websocket_code=ready')
29+
if process_ready:
30+
LogManager.logger.info(
31+
f"Exercise code {exercise_command} launched")
32+
time.sleep(1)
33+
self.exercise_connection = Client(
34+
'ws://127.0.0.1:1905', 'exercise', self.server_message)
35+
self.exercise_connection.start()
36+
else:
37+
self.exercise_server.kill()
38+
raise RuntimeError(f"Exercise {exercise_command} could not be run")
39+
40+
process_ready, self.gui_server = self._run_exercise_server(f"python3 {gui_command}", f'{home_dir}/ws_gui.log',
41+
'websocket_gui=ready')
42+
if process_ready:
43+
LogManager.logger.info(f"Exercise gui {gui_command} launched")
44+
time.sleep(1)
45+
self.gui_connection = Client(
46+
'ws://127.0.0.1:2303', 'gui', self.server_message)
47+
self.gui_connection.start()
48+
else:
49+
self.gui_server.kill()
50+
raise RuntimeError(f"Exercise GUI {gui_command} could not be run")
51+
52+
self.running = True
53+
54+
self.start_send_freq_thread()
55+
56+
57+
def send_freq(self, exercise_connection, is_alive):
58+
"""Send the frequency of the brain and gui to the exercise server"""
59+
while is_alive():
60+
exercise_connection.send(
61+
"""#freq{"brain": 20, "gui": 10, "rtf": 100}""")
62+
time.sleep(1)
63+
64+
def start_send_freq_thread(self):
65+
"""Start a thread to send the frequency of the brain and gui to the exercise server"""
66+
daemon = Thread(target=lambda: self.send_freq(self.exercise_connection,
67+
lambda: self.is_alive), daemon=False, name='Monitor frequencies')
68+
daemon.start()
69+
70+
def _run_exercise_server(self, cmd, log_file, load_string, timeout: int = 5):
71+
process = subprocess.Popen(f"{cmd}", shell=True, stdout=sys.stdout, stderr=subprocess.STDOUT,
72+
bufsize=1024, universal_newlines=True)
73+
74+
process_ready = False
75+
while not process_ready:
76+
try:
77+
f = open(log_file, "r")
78+
if f.readline() == load_string:
79+
process_ready = True
80+
f.close()
81+
time.sleep(0.2)
82+
except Exception as e:
83+
LogManager.logger.debug(
84+
f"waiting for server string '{load_string}'...")
85+
time.sleep(0.2)
86+
87+
return process_ready, process
88+
89+
def server_message(self, name, message):
90+
if name == "gui": # message received from GUI server
91+
LogManager.logger.debug(
92+
f"Message received from gui: {message[:30]}")
93+
self._process_gui_message(message)
94+
elif name == "exercise": # message received from EXERCISE server
95+
if message.startswith("#exec"):
96+
self.brain_ready_event.set()
97+
LogManager.logger.info(
98+
f"Message received from exercise: {message[:30]}")
99+
self._process_exercise_message(message)
100+
101+
def _process_gui_message(self, message):
102+
payload = json.loads(message[4:])
103+
self.update_callback(payload)
104+
self.gui_connection.send("#ack")
105+
106+
def _process_exercise_message(self, message):
107+
comand = message[:5]
108+
if (message==comand):
109+
payload = comand
110+
else:
111+
payload = json.loads(message[5:])
112+
self.update_callback(payload)
113+
self.exercise_connection.send("#ack")
114+
115+
def call_service(self, service, service_type):
116+
command = f"ros2 service call {service} {service_type}"
117+
subprocess.call(f"{command}", shell=True, stdout=sys.stdout, stderr=subprocess.STDOUT, bufsize=1024,
118+
universal_newlines=True)
119+
120+
def run(self):
121+
self.call_service("/unpause_physics","std_srvs/srv/Empty")
122+
self.exercise_connection.send("#play")
123+
124+
def stop(self):
125+
self.call_service("/pause_physics","std_srvs/srv/Empty")
126+
self.call_service("/reset_world","std_srvs/srv/Empty")
127+
self.exercise_connection.send("#rest")
128+
129+
def resume(self):
130+
self.call_service("/unpause_physics","std_srvs/srv/Empty")
131+
self.exercise_connection.send("#play")
132+
133+
def pause(self):
134+
self.call_service("/pause_physics","std_srvs/srv/Empty")
135+
self.exercise_connection.send("#stop")
136+
137+
def restart(self):
138+
# pause_cmd = "ros2 service call /restart_simulation std_srvs/srv/Empty"
139+
# subprocess.call(f"{pause_cmd}", shell=True, stdout=sys.stdout, stderr=subprocess.STDOUT, bufsize=1024,
140+
# universal_newlines=True)
141+
pass
142+
143+
@property
144+
def is_alive(self):
145+
return self.running
146+
147+
def load_code(self, code: str):
148+
errors = self.linter.evaluate_code(code)
149+
if errors == "":
150+
self.brain_ready_event.clear()
151+
self.exercise_connection.send(f"#code {code}")
152+
self.brain_ready_event.wait()
153+
else:
154+
raise Exception(errors)
155+
156+
def terminate(self):
157+
self.running = False
158+
self.exercise_connection.stop()
159+
self.gui_connection.stop()
160+
161+
stop_process_and_children(self.exercise_server)
162+
stop_process_and_children(self.gui_server)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from src.manager.manager.launcher.launcher_interface import ILauncher
2+
from src.manager.manager.docker_thread.docker_thread import DockerThread
3+
from src.manager.manager.vnc.vnc_server import Vnc_server
4+
import time
5+
import os
6+
import stat
7+
8+
9+
class LauncherConsoleRos2(ILauncher):
10+
display: str
11+
internal_port: str
12+
external_port: str
13+
running = False
14+
threads = []
15+
16+
def run(self, callback):
17+
DRI_PATH = os.path.join("/dev/dri", os.environ.get("DRI_NAME", "card0"))
18+
ACCELERATION_ENABLED = self.check_device(DRI_PATH)
19+
20+
console_vnc = Vnc_server()
21+
22+
if (ACCELERATION_ENABLED):
23+
console_vnc.start_vnc_gpu(self.display, self.internal_port, self.external_port,DRI_PATH)
24+
# Write display config and start the console
25+
console_cmd = f"export VGL_DISPLAY={DRI_PATH}; export DISPLAY={self.display}; vglrun xterm -fullscreen -sb -fa 'Monospace' -fs 10 -bg black -fg white"
26+
else:
27+
console_vnc.start_vnc(self.display, self.internal_port, self.external_port)
28+
# Write display config and start the console
29+
console_cmd = f"export DISPLAY={self.display};xterm -geometry 100x10+0+0 -fa 'Monospace' -fs 10 -bg black -fg white"
30+
31+
console_thread = DockerThread(console_cmd)
32+
console_thread.start()
33+
self.threads.append(console_thread)
34+
35+
self.running = True
36+
37+
def check_device(self, device_path):
38+
try:
39+
return stat.S_ISCHR(os.lstat(device_path)[stat.ST_MODE])
40+
except:
41+
return False
42+
43+
def is_running(self):
44+
return self.running
45+
46+
def terminate(self):
47+
for thread in self.threads:
48+
thread.terminate()
49+
thread.join()
50+
self.running = False
51+
52+
def died(self):
53+
pass
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from src.manager.manager.launcher.launcher_interface import ILauncher
2+
from src.manager.manager.docker_thread.docker_thread import DockerThread
3+
from src.manager.manager.vnc.vnc_server import Vnc_server
4+
import time
5+
import os
6+
import stat
7+
8+
9+
class LauncherGazeboViewRos2(ILauncher):
10+
exercise_id: str
11+
display: str
12+
internal_port: str
13+
external_port: str
14+
height: int
15+
width: int
16+
running = False
17+
threads = []
18+
19+
def run(self, callback):
20+
DRI_PATH = os.path.join(
21+
"/dev/dri", os.environ.get("DRI_NAME", "card0"))
22+
ACCELERATION_ENABLED = self.check_device(DRI_PATH)
23+
24+
# Configure browser screen width and height for gzclient
25+
gzclient_config_cmds = f"echo [geometry] > ~/.gazebo/gui.ini; echo x=0 >> ~/.gazebo/gui.ini; echo y=0 >> ~/.gazebo/gui.ini; echo width={self.width} >> ~/.gazebo/gui.ini; echo height={self.height} >> ~/.gazebo/gui.ini;"
26+
gz_vnc = Vnc_server()
27+
28+
if ACCELERATION_ENABLED:
29+
gz_vnc.start_vnc_gpu(self.display, self.internal_port, self.external_port, DRI_PATH)
30+
# Write display config and start gzclient
31+
gzclient_cmd = (
32+
f"export DISPLAY=:0; {gzclient_config_cmds} export VGL_DISPLAY={DRI_PATH}; vglrun gzclient --verbose")
33+
else:
34+
gz_vnc.start_vnc(self.display, self.internal_port, self.external_port)
35+
# Write display config and start gzclient
36+
gzclient_cmd = (
37+
f"export DISPLAY=:0; {gzclient_config_cmds} gzclient --verbose")
38+
39+
# wait for vnc and gazebo servers to load properly
40+
if (self.exercise_id == "follow_person_newmanager"):
41+
time.sleep(6)
42+
else:
43+
time.sleep(0.1)
44+
45+
gzclient_thread = DockerThread(gzclient_cmd)
46+
gzclient_thread.start()
47+
self.threads.append(gzclient_thread)
48+
49+
self.running = True
50+
51+
def check_device(self, device_path):
52+
try:
53+
return stat.S_ISCHR(os.lstat(device_path)[stat.ST_MODE])
54+
except:
55+
return False
56+
57+
def is_running(self):
58+
return self.running
59+
60+
def terminate(self):
61+
for thread in self.threads:
62+
thread.terminate()
63+
thread.join()
64+
self.running = False
65+
66+
def died(self):
67+
pass
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import os
2+
from typing import List, Any
3+
import time
4+
import stat
5+
6+
from src.manager.manager.launcher.launcher_interface import ILauncher, LauncherException
7+
from src.manager.manager.docker_thread.docker_thread import DockerThread
8+
import subprocess
9+
10+
import logging
11+
12+
class LauncherRos2Api(ILauncher):
13+
exercise_id: str
14+
type: str
15+
module: str
16+
resource_folders: List[str]
17+
model_folders: List[str]
18+
plugin_folders: List[str]
19+
parameters: List[str]
20+
launch_file: str
21+
running = False
22+
23+
def run(self,callback):
24+
DRI_PATH = os.path.join("/dev/dri", os.environ.get("DRI_NAME", "card0"))
25+
ACCELERATION_ENABLED = self.check_device(DRI_PATH)
26+
27+
logging.getLogger("roslaunch").setLevel(logging.CRITICAL)
28+
29+
# expand variables in configuration paths
30+
self._set_environment()
31+
launch_file = os.path.expandvars(self.launch_file)
32+
33+
if (ACCELERATION_ENABLED):
34+
exercise_launch_cmd = f"export VGL_DISPLAY={DRI_PATH}; vglrun ros2 launch {launch_file}"
35+
else:
36+
exercise_launch_cmd = f"ros2 launch {launch_file}"
37+
38+
exercise_launch_thread = DockerThread(exercise_launch_cmd)
39+
exercise_launch_thread.start()
40+
41+
self.running = True
42+
43+
def is_running(self):
44+
return self.running
45+
46+
def check_device(self, device_path):
47+
try:
48+
return stat.S_ISCHR(os.lstat(device_path)[stat.ST_MODE])
49+
except:
50+
return False
51+
52+
def terminate(self):
53+
if self.is_running():
54+
kill_cmd = 'pkill -9 -f '
55+
cmd = kill_cmd + 'gzserver'
56+
subprocess.call(cmd, shell=True, stdout=subprocess.PIPE, bufsize=1024, universal_newlines=True)
57+
cmd = kill_cmd + 'spawn_model.launch.py'
58+
subprocess.call(cmd, shell=True, stdout=subprocess.PIPE, bufsize=1024, universal_newlines=True)
59+
60+
def _set_environment(self):
61+
resource_folders = [os.path.expandvars(path) for path in self.resource_folders]
62+
model_folders = [os.path.expandvars(path) for path in self.model_folders]
63+
plugin_folders = [os.path.expandvars(path) for path in self.plugin_folders]
64+
65+
os.environ["GAZEBO_RESOURCE_PATH"] = f"{os.environ.get('GAZEBO_RESOURCE_PATH', '')}:{':'.join(resource_folders)}"
66+
os.environ["GAZEBO_MODEL_PATH"] = f"{os.environ.get('GAZEBO_MODEL_PATH', '')}:{':'.join(model_folders)}"
67+
os.environ["GAZEBO_PLUGIN_PATH"] = f"{os.environ.get('GAZEBO_PLUGIN_PATH', '')}:{':'.join(plugin_folders)}"

manager/manager/lint/linter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ def evaluate_code(self, code, warnings=False):
2727

2828
open("user_code.py", "r")
2929

30-
command = "export PYTHONPATH=$PYTHONPATH:/$EXERCISE_FOLDER/python_template/ros1_noetic; python3 RoboticsAcademy/src/manager/manager/lint/pylint_checker.py"
30+
command = ""
31+
output = subprocess.check_output(['bash', '-c', 'echo $ROS_VERSION'])
32+
output_str = output.decode('utf-8')
33+
version = output_str[0]
34+
if (version == 2):
35+
command = "export PYTHONPATH=$PYTHONPATH:/$EXERCISE_FOLDER/python_template/ros2_humble; python3 RoboticsAcademy/src/manager/manager/lint/pylint_checker.py"
36+
else:
37+
command = "export PYTHONPATH=$PYTHONPATH:/$EXERCISE_FOLDER/python_template/ros1_noetic; python3 RoboticsAcademy/src/manager/manager/lint/pylint_checker.py"
3138
ret = subprocess.run(command, capture_output=True, shell=True)
3239
result = ret.stdout.decode()
3340
result = result + "\n"

0 commit comments

Comments
 (0)