Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ The `Manager` class is the core of RAM, orchestrating operations and managing tr
4. **Error Handling**: `ManagerConsumer` communicates exceptions back to the client and `Manager`.
5. **Lifecycle Management**: `Manager` controls the start and stop of the `ManagerConsumer` WebSocket server.

#### Interaction Between `Manager` and `LauncherWorld`
#### Interaction Between `Manager` and `LauncherScene`

1. **World Initialization and Launching**: `Manager` initializes `LauncherWorld` with specific configurations, such as world type (e.g., `gazebo`, `drones`) and the launch file path.
2. **Dynamic Module Management**: `LauncherWorld` dynamically launches modules based on the world configuration and ROS version, as dictated by `Manager`.
3. **State Management and Transition**: The state of `Manager` is updated in response to the actions performed by `LauncherWorld`. For example, once the world is ready, `Manager` may transition to the `world_ready` state.
4. **Termination and Cleanup**: `Manager` can instruct `LauncherWorld` to terminate the world environment through its `terminate` method. `LauncherWorld` ensures a clean and orderly shutdown of all modules and resources involved in the world setup.
1. **World Initialization and Launching**: `Manager` initializes `LauncherScene` with specific configurations, such as world type (e.g., `gazebo`, `drones`) and the launch file path.
2. **Dynamic Module Management**: `LauncherScene` dynamically launches modules based on the world configuration and ROS version, as dictated by `Manager`.
3. **State Management and Transition**: The state of `Manager` is updated in response to the actions performed by `LauncherScene`. For example, once the world is ready, `Manager` may transition to the `world_ready` state.
4. **Termination and Cleanup**: `Manager` can instruct `LauncherScene` to terminate the world environment through its `terminate` method. `LauncherScene` ensures a clean and orderly shutdown of all modules and resources involved in the world setup.
5. **Error Handling and Logging**: `Manager` handles exceptions and errors that may arise during the world setup or termination processes, ensuring robust operation.

#### Interaction Between `Manager` and `LauncherTools`
Expand Down Expand Up @@ -110,32 +110,26 @@ The `Manager` class is the core of RAM, orchestrating operations and managing tr
## Usage Example

1. **Connecting to RAM**:

- Initially, the RAM is in the `idle` state.
- A client (e.g., a user interface or another system) connects to RAM, triggering the `connect` transition and moving RAM to the `connected` state.

2. **Launching the World**:

- Once connected, the client can request RAM to launch a robotic world by sending a `launch_world` command.
- RAM transitions to the `world_ready` state after successfully setting up the world environment.

3. **Setting Up Tools**:

- After the world is ready, the client requests RAM to prepare the tools with a `prepare_tools` command.
- RAM transitions to the `tools_ready` state, indicating that the tools are set up and ready.

4. **Running an Application**:

- The client then requests RAM to run a specific robotic application, moving RAM into the `application_running` state.
- The application executes, and RAM handles its process management, including monitoring and error handling.

5. **Pausing and Resuming Application**:

- The client can send `pause` and `resume` commands to RAM to control the application's execution.
- RAM transitions to the `paused` state when paused and returns to `application_running` upon resumption.

6. **Stopping the Application**:

- Finally, the client can send a `stop` command to halt the application.
- RAM stops the application and transitions back to the `tools_ready` state, ready for new commands.

Expand Down
2 changes: 1 addition & 1 deletion robotics_application_manager/manager/launcher/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .launcher_tools import LauncherTools
from .launcher_world import LauncherWorld
from .launcher_scene import LauncherScene
from .launcher_robot import LauncherRobot
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
}


class LauncherWorld(BaseModel):
class LauncherScene(BaseModel):
type: str
launch_file_path: str
module: str = ".".join(__name__.split(".")[:-1])
Expand All @@ -62,7 +62,7 @@ def run(self):
self.launchers.append(launcher)

def terminate(self):
LogManager.logger.info("Terminating worlds launchers")
LogManager.logger.info("Terminating scenes launchers")
if self.launchers:
for launcher in self.launchers:
launcher.terminate()
Expand All @@ -87,6 +87,6 @@ def launch_command(self, configuration):
pass


class LauncherWorldException(Exception):
class LauncherSceneException(Exception):
def __init__(self, message):
super(LauncherWorldException, self).__init__(message)
super(LauncherSceneException, self).__init__(message)
78 changes: 39 additions & 39 deletions robotics_application_manager/manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
)
from robotics_application_manager.ram_logging import LogManager
from robotics_application_manager.manager.launcher import (
LauncherWorld,
LauncherScene,
LauncherRobot,
LauncherTools,
)
Expand Down Expand Up @@ -121,10 +121,10 @@ class Manager:
"before": "on_terminate_tools",
},
{
"trigger": "terminate_universe",
"trigger": "terminate_world",
"source": "world_ready",
"dest": "connected",
"before": "on_terminate_universe",
"before": "on_terminate_world",
},
# Global transitions
{
Expand Down Expand Up @@ -222,7 +222,7 @@ def __init__(self, host: str, port: int):
self.ros_version = subprocess.check_output(["bash", "-c", "echo $ROS_DISTRO"])
self.queue = Queue()
self.consumer = ManagerConsumer(host, port, self.queue)
self.world_launcher = None
self.scene_launcher = None
self.world_type = None
self.robot_launcher = None
self.robot_config = None
Expand Down Expand Up @@ -307,7 +307,7 @@ def on_launch_world(self, event):
This method initializes the launch process based on the provided configuration.

During the launch process, it validates and processes the configuration data received from the event.
It then creates and starts a LauncherWorld instance with the validated configuration.
It then creates and starts a LauncherScene instance with the validated configuration.
This setup is crucial for preparing the environment and resources necessary for the application's execution.

Parameters:
Expand All @@ -322,30 +322,30 @@ def on_launch_world(self, event):
The method logs the start of the launch transition and the configuration details for debugging and traceability.
"""
cfg_dict = event.kwargs.get("data", {})
world_cfg = cfg_dict["world"]
scene_cfg = cfg_dict["scene"]
robot_cfg = cfg_dict["robot"]

# Launch world
# Launch scene
try:
if world_cfg["type"] == None:
self.world_launcher = None
if scene_cfg["type"] == None:
self.scene_launcher = None
LogManager.logger.info("Launch transition finished")
return
cfg = ConfigurationManager.validate(world_cfg)
if "zip" in world_cfg:
LogManager.logger.info("Launching universe from received zip")
self.prepare_custom_universe(world_cfg)
cfg = ConfigurationManager.validate(scene_cfg)
if "zip" in scene_cfg:
LogManager.logger.info("Launching scene from received zip")
self.prepare_custom_world(scene_cfg)
else:
LogManager.logger.info("Launching world from the RB")
LogManager.logger.info("Launching scene from the RB")

LogManager.logger.info(cfg)
except ValueError as e:
LogManager.logger.error(f"Configuration validation failed: {e}")

self.world_type = world_cfg["type"]
self.world_type = scene_cfg["type"]

self.world_launcher = LauncherWorld(**cfg.model_dump())
LogManager.logger.info(str(self.world_launcher))
self.scene_launcher = LauncherScene(**cfg.model_dump())
LogManager.logger.info(str(self.scene_launcher))

# Launch robot
self.robot_launcher = None
Expand All @@ -361,44 +361,44 @@ def on_launch_world(self, event):
self.robot_config = robot_cfg
LogManager.logger.info(str(self.robot_launcher))

self.world_launcher.run()
self.scene_launcher.run()
if self.robot_launcher is not None:
self.robot_launcher.run(
robot_cfg["entity"], robot_cfg["start_pose"], robot_cfg["extra_config"]
)
LogManager.logger.info("Launch transition finished")

def prepare_custom_universe(self, cfg_dict):
def prepare_custom_world(self, cfg_dict):
"""
Prepare and extract a custom universe from a base64-encoded zip file.
Prepare and extract a custom world from a base64-encoded zip file.

Then build it in the workspace.

Parameters:
cfg_dict (dict): Config dictionary containing the universe name and zip data
cfg_dict (dict): Config dictionary containing the world name and zip data
"""
# Unzip the app
if cfg_dict["zip"].startswith("data:"):
_, _, zip_file = cfg_dict["zip"].partition("base64,")
else:
zip_file = cfg_dict["zip"]

universe_ref = "/workspace/worlds/src/" + cfg_dict["name"]
world_ref = "/workspace/worlds/src/" + cfg_dict["name"]
# Remove old content
if os.path.exists("/workspace/worlds"):
shutil.rmtree("/workspace/worlds", ignore_errors=False)

# Create the folder if it doesn't exist
universe_folder = universe_ref + "/"
if not os.path.exists(universe_folder):
os.makedirs(universe_folder)
world_folder = world_ref + "/"
if not os.path.exists(world_folder):
os.makedirs(world_folder)

zip_destination = universe_ref + ".zip"
zip_destination = world_ref + ".zip"
with open(zip_destination, "wb") as result:
result.write(base64.b64decode(zip_file))

zip_ref = zipfile.ZipFile(zip_destination, "r")
zip_ref.extractall(universe_folder + "/")
zip_ref.extractall(world_folder + "/")
zip_ref.close()

os.system(
Expand Down Expand Up @@ -877,19 +877,19 @@ def on_terminate_tools(self, event):
self.tools_launcher.terminate()
self.tools_launcher = None

def on_terminate_universe(self, event):
def on_terminate_world(self, event):
"""
Handle the 'terminate_universe' event.
Handle the 'terminate_world' event.

Terminates the world and robot launchers if they exist
and terminates related Harmonic processes.

Parameters:
event: The event object associated with the termination request.
"""
if self.world_launcher is not None:
self.world_launcher.terminate()
self.world_launcher = None
if self.scene_launcher is not None:
self.scene_launcher.terminate()
self.scene_launcher = None
self.world_type = None
if self.robot_launcher is not None:
self.robot_launcher.terminate()
Expand Down Expand Up @@ -922,11 +922,11 @@ def on_disconnect(self, event):
except Exception as e:
LogManager.logger.exception("Exception terminating robot launcher")

if self.world_launcher:
if self.scene_launcher:
try:
self.world_launcher.terminate()
self.scene_launcher.terminate()
except Exception as e:
LogManager.logger.exception("Exception terminating world launcher")
LogManager.logger.exception("Exception terminating scene launcher")

def process_message(self, message):
if message.command == "gui":
Expand Down Expand Up @@ -1021,7 +1021,7 @@ def reset_sim(self):
self.robot_config["extra_config"],
)
except Exception as e:
LogManager.logger.exception("Exception terminating world launcher")
LogManager.logger.exception("Exception terminating scene launcher")

def start(self):
"""
Expand Down Expand Up @@ -1066,11 +1066,11 @@ def signal_handler(sign, frame):
except Exception as e:
LogManager.logger.exception("Exception terminating robot launcher")

if self.world_launcher:
if self.scene_launcher:
try:
self.world_launcher.terminate()
self.scene_launcher.terminate()
except Exception as e:
LogManager.logger.exception("Exception terminating world launcher")
LogManager.logger.exception("Exception terminating scene launcher")

exit()

Expand Down
6 changes: 3 additions & 3 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def dummy_run(self, start_pose=None, extra_config=None):
monkeypatch.setattr("os.makedirs", lambda path, exist_ok=False: None)
monkeypatch.setattr("os.path.isdir", lambda path: True)

# Patch LauncherWorld to avoid launching real processes
class DummyLauncherWorld:
# Patch LauncherScene to avoid launching real processes
class DummyLauncherScene:
def __init__(self, *a, **k):
self.launched = False

Expand All @@ -129,7 +129,7 @@ def terminate(self):
pass

monkeypatch.setattr(
"robotics_application_manager.manager.manager.LauncherWorld", DummyLauncherWorld
"robotics_application_manager.manager.manager.LauncherScene", DummyLauncherScene
)

class DummyFileWatchdog:
Expand Down
4 changes: 2 additions & 2 deletions test/test_connected_to_world_ready.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ def test_connected_to_world_ready(manager, monkeypatch):
# # Simulate logging error, but return a dummy config to avoid UnboundLocalError
# return DummyConfig()

# def fake_prepare_custom_universe(cfg):
# def fake_prepare_custom_world(cfg):
# raise ValueError("Invalid world configuration")

# monkeypatch.setattr(
# "robotics_application_manager.libs.launch_world_model.ConfigurationManager.validate",
# fake_validate,
# )
# manager.prepare_custom_universe = fake_prepare_custom_universe
# manager.prepare_custom_world = fake_prepare_custom_world

# event_data = {"world": invalid_world_cfg, "robot": valid_robot_cfg}

Expand Down
12 changes: 6 additions & 6 deletions test/test_terminate_transitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,23 @@ def test_terminate_tools_invalid_machine_error(manager, monkeypatch):
assert manager.state == "application_running"


def test_terminate_universe_valid(manager, monkeypatch):
"""Test the valid terminate_universe transition in the Manager."""
def test_terminate_world_valid(manager, monkeypatch):
"""Test the valid terminate_world transition in the Manager."""
# Ensure the manager is in a state where it can stop
setup_manager_to_world_ready(manager, monkeypatch)
# Mock needed methods and attributes
manager.visualization_launcher = DummyToolsLauncher()
manager.terminate_harmonic_processes = lambda: None

# Trigger the stop transition
manager.trigger("terminate_universe")
manager.trigger("terminate_world")
# Check that the state has changed to 'connected'
assert manager.state == "connected"


def test_terminate_universe_invalid_machine_error(manager, monkeypatch):
def test_terminate_world_invalid_machine_error(manager, monkeypatch):
"""
Test the invalid terminate_universe transition in the Manager.
Test the invalid terminate_world transition in the Manager.

Ensure that the transition raises an error when executed from an invalid state.
"""
Expand All @@ -141,6 +141,6 @@ def test_terminate_universe_invalid_machine_error(manager, monkeypatch):

# Trigger the stop transition
with pytest.raises(MachineError):
manager.trigger("terminate_universe")
manager.trigger("terminate_world")
# Check that the state has not changed
assert manager.state == "application_running"
4 changes: 2 additions & 2 deletions test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ def setup_manager_to_world_ready(manager, monkeypatch):
# State should now be 'world_ready'
assert manager.state == "world_ready"

# Patch LauncherWorld to avoid starting real worlds
# Patch LauncherScene to avoid starting real worlds
class DummyConsumer:
def __init__(self):
self.launched = False

def consume(self, *args, **kwargs):
pass

class DummyLauncherWorld:
class DummyLauncherScene:
def __init__(self, *args, **kwargs):
self.launched = False

Expand Down
Loading