diff --git a/manager/libs/launch_world_model.py b/manager/libs/launch_world_model.py index d59d8c3..8a017f3 100644 --- a/manager/libs/launch_world_model.py +++ b/manager/libs/launch_world_model.py @@ -6,7 +6,7 @@ class ConfigurationModel(BaseModel): - world: str + type: str launch_file_path: str diff --git a/manager/manager/docker_thread/docker_thread.py b/manager/manager/docker_thread/docker_thread.py index 1531088..27ba677 100644 --- a/manager/manager/docker_thread/docker_thread.py +++ b/manager/manager/docker_thread/docker_thread.py @@ -4,6 +4,7 @@ import subprocess import os import signal +import sys class DockerThread(threading.Thread): diff --git a/manager/manager/launcher/launcher_console.py b/manager/manager/launcher/launcher_console.py index 87a0b15..0611fef 100644 --- a/manager/manager/launcher/launcher_console.py +++ b/manager/manager/launcher/launcher_console.py @@ -12,6 +12,7 @@ class LauncherConsole(ILauncher): internal_port: int external_port: int running: bool = False + acceptsMsgs: bool = False threads: List[Any] = [] console_vnc: Any = Vnc_server() @@ -38,6 +39,15 @@ def run(self, config_file, callback): self.running = True + def pause(self): + pass + + def unpause(self): + pass + + def reset(self): + pass + def is_running(self): return self.running diff --git a/manager/manager/launcher/launcher_gazebo_view.py b/manager/manager/launcher/launcher_gazebo.py similarity index 78% rename from manager/manager/launcher/launcher_gazebo_view.py rename to manager/manager/launcher/launcher_gazebo.py index 54ed40e..c4a3e3a 100644 --- a/manager/manager/launcher/launcher_gazebo_view.py +++ b/manager/manager/launcher/launcher_gazebo.py @@ -1,3 +1,4 @@ +import sys from manager.manager.launcher.launcher_interface import ILauncher from manager.manager.docker_thread.docker_thread import DockerThread from manager.manager.vnc.vnc_server import Vnc_server @@ -12,13 +13,26 @@ from typing import List, Any -class LauncherGazeboView(ILauncher): +def call_service(service, service_type, request_data="{}"): + command = f"ros2 service call {service} {service_type} '{request_data}'" + subprocess.call( + f"{command}", + shell=True, + stdout=sys.stdout, + stderr=subprocess.STDOUT, + bufsize=1024, + universal_newlines=True, + ) + + +class LauncherGazebo(ILauncher): display: str internal_port: int external_port: int height: int width: int running: bool = False + acceptsMsgs: bool = False threads: List[Any] = [] gz_vnc: Any = Vnc_server() @@ -51,6 +65,15 @@ def run(self, config_file, callback): self.running = True + def pause(self): + call_service("/pause_physics", "std_srvs/srv/Empty") + + def unpause(self): + call_service("/unpause_physics", "std_srvs/srv/Empty") + + def reset(self): + call_service("/reset_world", "std_srvs/srv/Empty") + def is_running(self): return self.running diff --git a/manager/manager/launcher/launcher_gzsim_view.py b/manager/manager/launcher/launcher_gzsim.py similarity index 55% rename from manager/manager/launcher/launcher_gzsim_view.py rename to manager/manager/launcher/launcher_gzsim.py index 59d1fb3..a4296d2 100644 --- a/manager/manager/launcher/launcher_gzsim_view.py +++ b/manager/manager/launcher/launcher_gzsim.py @@ -1,3 +1,4 @@ +import sys from manager.manager.launcher.launcher_interface import ILauncher from manager.manager.docker_thread.docker_thread import DockerThread from manager.manager.vnc.vnc_server import Vnc_server @@ -10,9 +11,48 @@ import os import stat from typing import List, Any +from manager.ram_logging.log_manager import LogManager -class LauncherGzsimView(ILauncher): +def call_gzservice(service, reqtype, reptype, timeout, req): + command = f"gz service -s {service} --reqtype {reqtype} --reptype {reptype} --timeout {timeout} --req '{req}'" + subprocess.call( + f"{command}", + shell=True, + stdout=sys.stdout, + stderr=subprocess.STDOUT, + bufsize=1024, + universal_newlines=True, + ) + + +def call_service(service, service_type, request_data="{}"): + command = f"ros2 service call {service} {service_type} '{request_data}'" + subprocess.call( + f"{command}", + shell=True, + stdout=sys.stdout, + stderr=subprocess.STDOUT, + bufsize=1024, + universal_newlines=True, + ) + + +def is_ros_service_available(service_name): + try: + result = subprocess.run( + ["ros2", "service", "list", "--include-hidden-services"], + capture_output=True, + text=True, + check=True, + ) + return service_name in result.stdout + except subprocess.CalledProcessError as e: + LogManager.logger.exception(f"Error checking service availability: {e}") + return False + + +class LauncherGzsim(ILauncher): display: str internal_port: int external_port: int @@ -73,6 +113,41 @@ def terminate(self): def died(self): pass + def pause(self): + call_gzservice( + "$(gz service -l | grep '^/world/\w*/control$')", + "gz.msgs.WorldControl", + "gz.msgs.Boolean", + "3000", + "pause: true", + ) + + def unpause(self): + call_gzservice( + "$(gz service -l | grep '^/world/\w*/control$')", + "gz.msgs.WorldControl", + "gz.msgs.Boolean", + "3000", + "pause: false", + ) + + def reset(self): + if is_ros_service_available("/drone0/platform/state_machine/_reset"): + call_service( + "/drone0/platform/state_machine/_reset", + "std_srvs/srv/Trigger", + "{}", + ) + call_gzservice( + "$(gz service -l | grep '^/world/\w*/control$')", + "gz.msgs.WorldControl", + "gz.msgs.Boolean", + "3000", + "reset: {all: true}", + ) + if is_ros_service_available("/drone0/controller/_reset"): + call_service("/drone0/controller/_reset", "std_srvs/srv/Trigger", "{}") + def get_dri_path(self): directory_path = "/dev/dri" dri_path = "" diff --git a/manager/manager/launcher/launcher_robot.py b/manager/manager/launcher/launcher_robot.py index 00826af..8519f67 100644 --- a/manager/manager/launcher/launcher_robot.py +++ b/manager/manager/launcher/launcher_robot.py @@ -66,7 +66,7 @@ class LauncherRobot(BaseModel): - world: str + type: str launch_file_path: str module: str = ".".join(__name__.split(".")[:-1]) ros_version: int = get_ros_version() @@ -76,7 +76,7 @@ class LauncherRobot(BaseModel): def run(self, start_pose=None): if start_pose != None: self.start_pose = start_pose - for module in worlds[self.world][str(self.ros_version)]: + for module in worlds[self.type][str(self.ros_version)]: module["launch_file"] = self.launch_file_path launcher = self.launch_module(module) self.launchers.append(launcher) diff --git a/manager/manager/launcher/launcher_state_monitor.py b/manager/manager/launcher/launcher_state_monitor.py new file mode 100644 index 0000000..8e645b5 --- /dev/null +++ b/manager/manager/launcher/launcher_state_monitor.py @@ -0,0 +1,52 @@ +from manager.libs.applications.compatibility.server import Server +from manager.ram_logging.log_manager import LogManager +from manager.comms.new_consumer import ManagerConsumer +from typing import Optional +from manager.libs.applications.compatibility.file_watchdog import FileWatchdog + + +class LauncherStateMonitor: + file: str + consumer: ManagerConsumer + running: bool = False + acceptsMsgs: bool = True + + def __init__(self, type, module, file, consumer): + self.file = file + self.consumer = consumer + self.server = FileWatchdog("/tmp/tree_state", self.update) + + def update(self, data): + LogManager.logger.debug(f"Sending update to client") + if self.consumer is not None: + self.consumer.send_message({"update": data}, command="update") + + def run(self, config_file, callback): + self.server.start() + self.running = True + + def get_msg(self, data): + self.server.send(data) + + def is_running(self): + return self.running + + def terminate(self): + self.server.stop() + self.running = False + + def pause(self): + pass + + def unpause(self): + pass + + def reset(self): + pass + + def died(self): + pass + + def from_config(cls, config): + obj = cls(**config) + return obj diff --git a/manager/manager/launcher/launcher_tools.py b/manager/manager/launcher/launcher_tools.py new file mode 100644 index 0000000..4eef9b0 --- /dev/null +++ b/manager/manager/launcher/launcher_tools.py @@ -0,0 +1,123 @@ +from manager.libs.process_utils import get_class, class_from_module +from typing import Optional +from pydantic import BaseModel + + +from manager.libs.process_utils import get_class, class_from_module +from manager.ram_logging.log_manager import LogManager +from manager.manager.launcher.launcher_interface import ILauncher + +tools = { + "console": { + "module": "console", + "display": ":1", + "external_port": 1108, + "internal_port": 5901, + }, + "gazebo": { + "type": "module", + "width": 1024, + "height": 768, + "module": "gazebo", + "display": ":2", + "external_port": 6080, + "internal_port": 5900, + }, + "gzsim": { + "type": "module", + "width": 1024, + "height": 768, + "module": "gzsim", + "display": ":2", + "external_port": 6080, + "internal_port": 5900, + }, + "web_gui": { + "type": "module", + "module": "web_gui", + "internal_port": 2303, + "consumer": None, + }, + "state_monitor": { + "type": "module", + "module": "state_monitor", + "file": "/tmp/tree_state", + "consumer": None, + }, +} + +simulator = { + "gazebo": {"tool": "gazebo"}, + "gz": {"tool": "gzsim"}, +} + + +class LauncherTools(BaseModel): + module: str = ".".join(__name__.split(".")[:-1]) + world_type: Optional[str] = None + tools: list[str] + tools_config: Optional[dict] = None + launchers: Optional[ILauncher] = [] + + def run(self, consumer): + for tool in self.tools: + if tool == "simulator": + tool = simulator[self.world_type]["tool"] + module = tools[tool] + launcher = self.launch_module(tool, module, consumer) + self.launchers.append(launcher) + + def terminate(self): + LogManager.logger.info("Terminating tools launcher") + for launcher in self.launchers: + if launcher.is_running(): + launcher.terminate() + self.launchers = [] + + def launch_module(self, name, configuration, consumer): + def process_terminated(name, exit_code): + LogManager.logger.info( + f"LauncherEngine: {name} exited with code {exit_code}" + ) + if self.terminated_callback is not None: + self.terminated_callback(name, exit_code) + + # Replace consumer + if "consumer" in configuration: + configuration["consumer"] = consumer + + launcher_module_name = configuration["module"] + launcher_module = f"{self.module}.launcher_{launcher_module_name}.Launcher{class_from_module(launcher_module_name)}" + launcher_class = get_class(launcher_module) + config = None + if self.tools_config is not None and name in self.tools_config: + config = self.tools_config[name] + + launcher = launcher_class.from_config(launcher_class, configuration) + launcher.run(config, process_terminated) + return launcher + + def pause(self): + for launcher in self.launchers: + launcher.pause() + + def unpause(self): + for launcher in self.launchers: + launcher.unpause() + + def reset(self): + for launcher in self.launchers: + launcher.reset() + + def pass_msg(self, data): + for launcher in self.launchers: + if launcher.acceptsMsgs: + launcher.get_msg(data) + + def launch_command(self, configuration): + pass + + +class LauncherToolsException(Exception): + def __init__(self, message): + super(LauncherToolsException, self).__init__(message) diff --git a/manager/manager/launcher/launcher_visualization.py b/manager/manager/launcher/launcher_visualization.py deleted file mode 100644 index 369cac5..0000000 --- a/manager/manager/launcher/launcher_visualization.py +++ /dev/null @@ -1,223 +0,0 @@ -from manager.libs.process_utils import get_class, class_from_module -from typing import Optional -from pydantic import BaseModel - - -from manager.libs.process_utils import get_class, class_from_module, get_ros_version -from manager.ram_logging.log_manager import LogManager -from manager.manager.launcher.launcher_interface import ILauncher - - -visualization = { - "none": [], - "console": [ - { - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - } - ], - "bt_studio": [ - { - "type": "module", - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "gazebo_view", - "display": ":2", - "external_port": 6080, - "internal_port": 5900, - }, - ], - "bt_studio_gz": [ - { - "type": "module", - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "gzsim_view", - "display": ":2", - "external_port": 6080, - "internal_port": 5900, - }, - ], - "gazebo_gra": [ - { - "type": "module", - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "gazebo_view", - "display": ":2", - "external_port": 6080, - "internal_port": 5900, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "robot_display_view", - "display": ":3", - "external_port": 2303, - "internal_port": 5902, - }, - ], - "gazebo_rae": [ - { - "type": "module", - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "gazebo_view", - "display": ":2", - "external_port": 6080, - "internal_port": 5900, - }, - ], - "gzsim_gra": [ - { - "type": "module", - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "gzsim_view", - "display": ":2", - "external_port": 6080, - "internal_port": 5900, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "robot_display_view", - "display": ":3", - "external_port": 2303, - "internal_port": 5902, - }, - ], - "gzsim_rae": [ - { - "type": "module", - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "gzsim_view", - "display": ":2", - "external_port": 6080, - "internal_port": 5900, - }, - ], - "physic_gra": [ - { - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "robot_display_view", - "display": ":2", - "external_port": 2303, - "internal_port": 5902, - }, - ], - "physic_rae": [ - { - "module": "console", - "display": ":1", - "external_port": 1108, - "internal_port": 5901, - }, - { - "type": "module", - "width": 1024, - "height": 768, - "module": "robot_display_view", - "display": ":2", - "external_port": 2303, - "internal_port": 5902, - }, - ], -} - - -class LauncherVisualization(BaseModel): - module: str = ".".join(__name__.split(".")[:-1]) - visualization: str - visualization_config_path: Optional[str] = None - launchers: Optional[ILauncher] = [] - - def run(self): - for module in visualization[self.visualization]: - launcher = self.launch_module(module) - self.launchers.append(launcher) - - def terminate(self): - LogManager.logger.info("Terminating visualization launcher") - for launcher in self.launchers: - if launcher.is_running(): - launcher.terminate() - self.launchers = [] - - def launch_module(self, configuration): - def process_terminated(name, exit_code): - LogManager.logger.info( - f"LauncherEngine: {name} exited with code {exit_code}" - ) - if self.terminated_callback is not None: - self.terminated_callback(name, exit_code) - - launcher_module_name = configuration["module"] - launcher_module = f"{self.module}.launcher_{launcher_module_name}.Launcher{class_from_module(launcher_module_name)}" - launcher_class = get_class(launcher_module) - launcher = launcher_class.from_config(launcher_class, configuration) - launcher.run(self.visualization_config_path, process_terminated) - return launcher - - def launch_command(self, configuration): - pass - - -class LauncherVisualizationException(Exception): - def __init__(self, message): - super(LauncherWorldException, self).__init__(message) diff --git a/manager/manager/launcher/launcher_web_gui.py b/manager/manager/launcher/launcher_web_gui.py new file mode 100644 index 0000000..6d5c9e3 --- /dev/null +++ b/manager/manager/launcher/launcher_web_gui.py @@ -0,0 +1,51 @@ +from manager.libs.applications.compatibility.server import Server +from manager.ram_logging.log_manager import LogManager +from manager.comms.new_consumer import ManagerConsumer +from typing import Optional + + +class LauncherWebGui: + internal_port: int + consumer: ManagerConsumer + running: bool = False + acceptsMsgs: bool = True + + def __init__(self, type, module, internal_port, consumer): + self.internal_port = internal_port + self.consumer = consumer + self.server = Server(self.internal_port, self.update) + + def update(self, data): + LogManager.logger.debug(f"Sending update to client") + if self.consumer is not None: + self.consumer.send_message({"update": data}, command="update") + + def run(self, config_file, callback): + self.server.start() + self.running = True + + def get_msg(self, data): + self.server.send(data) + + def is_running(self): + return self.running + + def terminate(self): + self.server.stop() + self.running = False + + def pause(self): + pass + + def unpause(self): + pass + + def reset(self): + pass + + def died(self): + pass + + def from_config(cls, config): + obj = cls(**config) + return obj diff --git a/manager/manager/launcher/launcher_world.py b/manager/manager/launcher/launcher_world.py index b96f434..9ff62c5 100644 --- a/manager/manager/launcher/launcher_world.py +++ b/manager/manager/launcher/launcher_world.py @@ -16,27 +16,11 @@ } ], }, - "drones": { + "gz": { "2": [ { "type": "module", - "module": "drones_ros2", - "resource_folders": [], - "model_folders": [], - "plugin_folders": [], - "parameters": [], - "launch_file": [], - } - ], - }, - "gzsimdrones": { - "2": [ - { - "type": "module", - "module": "drones_gzsim", - "resource_folders": [], - "model_folders": [], - "plugin_folders": [], + "module": "ros2_api", "parameters": [], "launch_file": [], } @@ -47,14 +31,14 @@ class LauncherWorld(BaseModel): - world: str + type: str launch_file_path: str module: str = ".".join(__name__.split(".")[:-1]) ros_version: int = get_ros_version() launchers: Optional[ILauncher] = [] def run(self): - for module in worlds[self.world][str(self.ros_version)]: + for module in worlds[self.type][str(self.ros_version)]: module["launch_file"] = self.launch_file_path launcher = self.launch_module(module) self.launchers.append(launcher) diff --git a/manager/manager/lint/linter.py b/manager/manager/lint/linter.py index d56ed78..63c6966 100644 --- a/manager/manager/lint/linter.py +++ b/manager/manager/lint/linter.py @@ -1,6 +1,8 @@ +import glob import re import os import subprocess +import tempfile class Lint: @@ -102,3 +104,56 @@ def evaluate_code( return final_result.strip() except Exception as ex: print(ex) + + def evaluate_source_code(self, files): + all_files = [] + for f in files: + glob_files = glob.glob(os.path.join("/workspace/code", f)) + for g in glob_files: + all_files.append(g) + + output = [] + + linter_env = os.environ.copy() + linter_env["PYTHONPATH"] = f"{linter_env['PYTHONPATH']}:/workspace/code" + + try: + + for file in all_files: + if file.endswith(".py"): + with open(file) as f: + python_code = f.read() + + # Create temp file + code_file = tempfile.NamedTemporaryFile(delete=False) + code_file.write(python_code.encode()) + code_file.seek(0) + code_file.close() + + options = ( + f"{code_file.name} --enable=similarities --disable=C0114,C0116" + ) + + # Run pylint using subprocess + result = subprocess.run( + ["pylint"] + options.split(), + capture_output=True, + text=True, + env=linter_env, + ) + + # Process pylint exit + stdout = result.stdout + + # Clean temp files + if os.path.exists(code_file.name): + os.remove(code_file.name) + + raw_output = stdout + "\n" + cleaned_result = self.clean_pylint_output(raw_output) + final_result = self.append_rating_if_missing(cleaned_result) + output.append(final_result.strip()) + + return output + except Exception as ex: + return ex diff --git a/manager/manager/manager.py b/manager/manager/manager.py index 6a34c61..e9c1591 100644 --- a/manager/manager/manager.py +++ b/manager/manager/manager.py @@ -32,10 +32,8 @@ from manager.libs.launch_world_model import ConfigurationManager from manager.manager.launcher.launcher_world import LauncherWorld from manager.manager.launcher.launcher_robot import LauncherRobot -from manager.manager.launcher.launcher_visualization import LauncherVisualization +from manager.manager.launcher.launcher_tools import LauncherTools from manager.ram_logging.log_manager import LogManager -from manager.libs.applications.compatibility.server import Server -from manager.libs.applications.compatibility.file_watchdog import FileWatchdog from manager.manager.application.robotics_python_application_interface import ( IRoboticsPythonApplication, ) @@ -49,7 +47,7 @@ class Manager: "idle", "connected", "world_ready", - "visualization_ready", + "tools_ready", "application_running", "paused", ] @@ -71,15 +69,15 @@ class Manager: }, # Transitions for state world ready { - "trigger": "prepare_visualization", + "trigger": "prepare_tools", "source": "world_ready", - "dest": "visualization_ready", - "before": "on_prepare_visualization", + "dest": "tools_ready", + "before": "on_prepare_tools", }, - # Transitions for state visualization_ready + # Transitions for state tools_ready { "trigger": "run_application", - "source": ["visualization_ready", "paused", "application_running"], + "source": ["tools_ready", "paused", "application_running"], "dest": "application_running", "before": "on_run_application", }, @@ -99,15 +97,15 @@ class Manager: # Transitions for terminate levels { "trigger": "terminate_application", - "source": ["visualization_ready", "application_running", "paused"], - "dest": "visualization_ready", + "source": ["tools_ready", "application_running", "paused"], + "dest": "tools_ready", "before": "on_terminate_application", }, { - "trigger": "terminate_visualization", - "source": "visualization_ready", + "trigger": "terminate_tools", + "source": "tools_ready", "dest": "world_ready", - "before": "on_terminate_visualization", + "before": "on_terminate_tools", }, { "trigger": "terminate_universe", @@ -130,7 +128,7 @@ class Manager: "connected", "paused", "world_ready", - "visualization_ready", + "tools_ready", ], "dest": "=", "before": "on_style_check_application", @@ -143,7 +141,7 @@ class Manager: "connected", "paused", "world_ready", - "visualization_ready", + "tools_ready", ], "dest": "=", "before": "on_code_analysis", @@ -156,7 +154,7 @@ class Manager: "connected", "paused", "world_ready", - "visualization_ready", + "tools_ready", ], "dest": "=", "before": "on_code_format", @@ -169,7 +167,7 @@ class Manager: "connected", "paused", "world_ready", - "visualization_ready", + "tools_ready", ], "dest": "=", "before": "on_code_autocomplete", @@ -190,12 +188,11 @@ def __init__(self, host: str, port: int): self.queue = Queue() self.consumer = ManagerConsumer(host, port, self.queue) self.world_launcher = None + self.world_type = None self.robot_launcher = None - self.visualization_launcher = None - self.visualization_type = None + self.tools_launcher = None self.application_process = None self.running = True - self.gui_server = None self.linter = Lint() # Creates workspace directories @@ -274,7 +271,7 @@ def on_launch_world(self, event): # Launch world try: - if world_cfg["world"] == None: + if world_cfg["type"] == None: self.world_launcher = None LogManager.logger.info("Launch transition finished") return @@ -289,6 +286,8 @@ def on_launch_world(self, event): except ValueError as e: LogManager.logger.error(f"Configuration validation failed: {e}") + self.world_type = world_cfg["type"] + self.world_launcher = LauncherWorld(**cfg.model_dump()) LogManager.logger.info(str(self.world_launcher)) self.world_launcher.run() @@ -296,7 +295,7 @@ def on_launch_world(self, event): # Launch robot try: - if robot_cfg["world"] == None: + if robot_cfg["type"] == None: self.robot_launcher = None LogManager.logger.info("Launch transition finished") return @@ -342,58 +341,20 @@ def prepare_custom_universe(self, cfg_dict): '/bin/bash -c "cd /workspace/worlds; source /opt/ros/humble/setup.bash; colcon build --symlink-install; source install/setup.bash; cd ../.."' ) - def on_prepare_visualization(self, event): + def on_prepare_tools(self, event): - LogManager.logger.info("Visualization transition started") + LogManager.logger.info("Tools transition started") cfg_dict = event.kwargs.get("data", {}) - self.visualization_type = cfg_dict["type"] - config_file = cfg_dict["file"] - - self.visualization_launcher = LauncherVisualization( - visualization=self.visualization_type, visualization_config_path=config_file - ) - - self.visualization_launcher.run() - - if self.visualization_type in ["gazebo_rae", "gzsim_rae", "console"]: - self.gui_server = Server(2303, self.update) - self.gui_server.start() - elif self.visualization_type in ["bt_studio", "bt_studio_gz"]: - self.gui_server = FileWatchdog("/tmp/tree_state", self.update_bt_studio) - self.gui_server.start() - - LogManager.logger.info("Visualization transition finished") + tools = cfg_dict["tools"] + config = cfg_dict["config"] - def add_frequency_control(self, code): - frequency_control_code_imports = """ -import time -from datetime import datetime -ideal_cycle = 20 -""" - code = frequency_control_code_imports + code - infinite_loop = re.search( - r"[^ ]while\s*\(\s*True\s*\)\s*:|[^ ]while\s*True\s*:|[^ ]while\s*1\s*:|[^ ]while\s*\(\s*1\s*\)\s*:", - code, - ) - frequency_control_code_pre = """ - start_time_internal_freq_control = datetime.now() - """ - code = ( - code[: infinite_loop.end()] - + frequency_control_code_pre - + code[infinite_loop.end() :] + self.tools_launcher = LauncherTools( + world_type=self.world_type, tools=tools, tools_config=config ) - frequency_control_code_post = """ - finish_time_internal_freq_control = datetime.now() - dt = finish_time_internal_freq_control - start_time_internal_freq_control - ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0 - if (ms < ideal_cycle): - time.sleep((ideal_cycle - ms) / 1000.0) -""" - code = code + frequency_control_code_post - return code + self.tools_launcher.run(self.consumer) + LogManager.logger.info("Tools transition finished") def on_style_check_application(self, event): """ @@ -627,32 +588,6 @@ def find_docker_console(): # raise Exception("No active console other than /dev/pts/0") return consoles - def prepare_RA_code(code_path): - f = open(code_path, "r") - code = f.read() - f.close() - - # Make code backwards compatible - code = code.replace("from GUI import GUI", "import GUI") - code = code.replace("from HAL import HAL", "import HAL") - - # Create executable app - errors = self.linter.evaluate_code(code, self.ros_version) - if errors == "": - - code = self.add_frequency_control(code) - f = open(code_path, "w") - f.write(code) - f.close() - - else: - console_path = find_docker_console() - for i in console_path: - with open(i, "w") as console: - console.write(errors + "\n\n") - - raise Exception(errors) - # Kill already running code try: proc = psutil.Process(self.application_process.pid) @@ -668,12 +603,8 @@ def prepare_RA_code(code_path): # Extract app config app_cfg = event.kwargs.get("data", {}) - type = app_cfg["type"] - - if type == "robotics-academy": - code_path = "/workspace/code/academy.py" - elif type == "bt-studio": - code_path = "/workspace/code/execute_docker.py" + entrypoint = app_cfg["entrypoint"] + to_lint = app_cfg["linter"] # Unzip the app if app_cfg["code"].startswith("data:"): @@ -684,19 +615,31 @@ def prepare_RA_code(code_path): zip_ref.extractall("/workspace/code") zip_ref.close() - if not os.path.isfile(code_path): + if not os.path.isfile(entrypoint): LogManager.logger.info("User code not found") raise Exception("User code not found") - try: - if type == "robotics-academy": - prepare_RA_code(code_path) + # Pass the linter + errors = self.linter.evaluate_source_code(to_lint) + failed_linter = False + for error in errors: + if error != "": + failed_linter = True + console_path = find_docker_console() + for i in console_path: + with open(i, "w") as console: + console.write(error + "\n\n") + + if failed_linter: + raise Exception(errors) + + try: fds = os.listdir("/dev/pts/") console_fd = str(max(map(int, fds[:-1]))) self.application_process = subprocess.Popen( - ["python3", code_path], + ["python3", entrypoint], stdin=open("/dev/pts/" + console_fd, "r"), stdout=sys.stdout, stderr=subprocess.STDOUT, @@ -767,12 +710,9 @@ def on_terminate_application(self, event): LogManager.logger.exception("No application running") print(traceback.format_exc()) - def on_terminate_visualization(self, event): + def on_terminate_tools(self, event): - self.visualization_launcher.terminate() - if self.gui_server != None: - self.gui_server.stop() - self.gui_server = None + self.tools_launcher.terminate() self.terminate_harmonic_processes() def on_terminate_universe(self, event): @@ -797,13 +737,11 @@ def on_disconnect(self, event): except Exception as e: LogManager.logger.exception("Exception stopping application process") - if self.visualization_launcher: + if self.tools_launcher: try: - self.visualization_launcher.terminate() + self.tools_launcher.terminate() except Exception as e: - LogManager.logger.exception( - "Exception terminating visualization launcher" - ) + LogManager.logger.exception("Exception terminating tools launcher") if self.robot_launcher: try: @@ -825,7 +763,7 @@ def on_disconnect(self, event): def process_message(self, message): if message.command == "gui": - self.gui_server.send(message.data) + self.tools_launcher.pass_msg(message.data) return self.trigger(message.command, data=message.data or None) @@ -862,53 +800,16 @@ def on_resume(self, msg): self.reset_sim() def pause_sim(self): - if self.visualization_type in ["gzsim_rae", "bt_studio_gz"]: - self.call_gzservice( - "$(gz service -l | grep '^/world/\w*/control$')", - "gz.msgs.WorldControl", - "gz.msgs.Boolean", - "3000", - "pause: true", - ) - elif not self.visualization_type in ["console"]: - self.call_service("/pause_physics", "std_srvs/srv/Empty") + self.tools_launcher.pause() def unpause_sim(self): - if self.visualization_type in ["gzsim_rae", "bt_studio_gz"]: - self.call_gzservice( - "$(gz service -l | grep '^/world/\w*/control$')", - "gz.msgs.WorldControl", - "gz.msgs.Boolean", - "3000", - "pause: false", - ) - elif not self.visualization_type in ["console"]: - self.call_service("/unpause_physics", "std_srvs/srv/Empty") + self.tools_launcher.unpause() def reset_sim(self): if self.robot_launcher: self.robot_launcher.terminate() - if self.visualization_type in ["gzsim_rae", "bt_studio_gz"]: - if self.is_ros_service_available("/drone0/platform/state_machine/_reset"): - self.call_service( - "/drone0/platform/state_machine/_reset", - "std_srvs/srv/Trigger", - "{}", - ) - self.call_gzservice( - "$(gz service -l | grep '^/world/\w*/control$')", - "gz.msgs.WorldControl", - "gz.msgs.Boolean", - "3000", - "reset: {all: true}", - ) - if self.is_ros_service_available("/drone0/controller/_reset"): - self.call_service( - "/drone0/controller/_reset", "std_srvs/srv/Trigger", "{}" - ) - elif not self.visualization_type in ["console"]: - self.call_service("/reset_world", "std_srvs/srv/Empty") + self.tools_launcher.reset() if self.robot_launcher: try: @@ -916,41 +817,6 @@ def reset_sim(self): except Exception as e: LogManager.logger.exception("Exception terminating world launcher") - def call_service(self, service, service_type, request_data="{}"): - command = f"ros2 service call {service} {service_type} '{request_data}'" - subprocess.call( - f"{command}", - shell=True, - stdout=sys.stdout, - stderr=subprocess.STDOUT, - bufsize=1024, - universal_newlines=True, - ) - - def call_gzservice(self, service, reqtype, reptype, timeout, req): - command = f"gz service -s {service} --reqtype {reqtype} --reptype {reptype} --timeout {timeout} --req '{req}'" - subprocess.call( - f"{command}", - shell=True, - stdout=sys.stdout, - stderr=subprocess.STDOUT, - bufsize=1024, - universal_newlines=True, - ) - - def is_ros_service_available(self, service_name): - try: - result = subprocess.run( - ["ros2", "service", "list", "--include-hidden-services"], - capture_output=True, - text=True, - check=True, - ) - return service_name in result.stdout - except subprocess.CalledProcessError as e: - LogManager.logger.exception(f"Error checking service availability: {e}") - return False - def start(self): """ Starts the RAM @@ -965,11 +831,6 @@ def start(self): def signal_handler(sign, frame): print("\nprogram exiting gracefully") self.running = False - if self.gui_server is not None: - try: - self.gui_server.stop() - except Exception as e: - LogManager.logger.exception("Exception stopping GUI server") try: self.consumer.stop() @@ -985,13 +846,11 @@ def signal_handler(sign, frame): "Exception stopping application process" ) - if self.visualization_launcher: + if self.tools_launcher: try: - self.visualization_launcher.terminate() + self.tools_launcher.terminate() except Exception as e: - LogManager.logger.exception( - "Exception terminating visualization launcher" - ) + LogManager.logger.exception("Exception terminating tools launcher") if self.robot_launcher: try: