2024-10-25 14:49:07 -03:00
|
|
|
#!/bin/env python3
|
|
|
|
import typer
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import contextlib
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
from typing import List
|
|
|
|
from loguru import logger
|
|
|
|
import sh
|
|
|
|
import atexit
|
|
|
|
|
|
|
|
load_dotenv()
|
|
|
|
logger.remove()
|
|
|
|
logger.add(
|
|
|
|
sys.stdout, format="<green>{time}</green> <level>{level}</level> {extra} {message}"
|
|
|
|
)
|
|
|
|
|
|
|
|
# px4Path = os.environ.get("SPIRI_SIM_PX4_PATH", "/opt/spiri-sdk/PX4-Autopilot/")
|
|
|
|
# logger.info(f"SPIRI_SIM_PX4_PATH={px4Path}")
|
|
|
|
|
|
|
|
app = typer.Typer()
|
|
|
|
|
|
|
|
# This is a list of processes that we need to .kill and .wait for on exit
|
|
|
|
processes = []
|
|
|
|
|
|
|
|
|
|
|
|
class outputLogger:
|
|
|
|
"""
|
|
|
|
Logs command output to loguru
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, name, instance):
|
|
|
|
self.name = name
|
|
|
|
self.instance = instance
|
|
|
|
|
|
|
|
def __call__(self, message):
|
|
|
|
with logger.contextualize(cmd=self.name, instance=self.instance):
|
|
|
|
if message.endswith("\n"):
|
|
|
|
message = message[:-1]
|
|
|
|
# ToDo, this doesn't work because the output is coloured
|
|
|
|
if message.startswith("INFO"):
|
|
|
|
message = message.lstrip("INFO")
|
|
|
|
logger.info(message)
|
|
|
|
elif message.startswith("WARN"):
|
|
|
|
message = message.lstrip("WARN")
|
|
|
|
logger.warning(message)
|
|
|
|
elif message.startswith("ERROR"):
|
|
|
|
message = message.lstrip("ERROR")
|
|
|
|
logger.error(message)
|
|
|
|
elif message.startswith("DEBUG"):
|
|
|
|
message = message.lstrip("DEBUG")
|
|
|
|
logger.debug(message)
|
|
|
|
else:
|
|
|
|
logger.info(message)
|
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def modified_environ(*remove, **update):
|
|
|
|
"""
|
|
|
|
Temporarily updates the ``os.environ`` dictionary in-place.
|
|
|
|
|
|
|
|
The ``os.environ`` dictionary is updated in-place so that the modification
|
|
|
|
is sure to work in all situations.
|
|
|
|
|
|
|
|
:param remove: Environment variables to remove.
|
|
|
|
:param update: Dictionary of environment variables and values to add/update.
|
|
|
|
"""
|
|
|
|
env = os.environ
|
|
|
|
update = update or {}
|
|
|
|
remove = remove or []
|
|
|
|
|
|
|
|
# List of environment variables being updated or removed.
|
|
|
|
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
|
|
|
|
# Environment variables and values to restore on exit.
|
|
|
|
update_after = {k: env[k] for k in stomped}
|
|
|
|
# Environment variables and values to remove on exit.
|
|
|
|
remove_after = frozenset(k for k in update if k not in env)
|
|
|
|
|
|
|
|
try:
|
|
|
|
env.update(update)
|
|
|
|
[env.pop(k, None) for k in remove]
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
env.update(update_after)
|
|
|
|
[env.pop(k) for k in remove_after]
|
|
|
|
|
|
|
|
|
2024-11-01 12:20:39 -03:00
|
|
|
# @app.command()
|
2024-11-04 17:29:16 -04:00
|
|
|
def start(instance: int = 0, sys_id: int = 1):
|
2024-10-25 14:49:07 -03:00
|
|
|
"""Starts the simulated drone with a given sys_id,
|
|
|
|
each drone must have it's own unique ID.
|
|
|
|
"""
|
|
|
|
if sys_id < 1 or sys_id > 254:
|
|
|
|
logger.error("sys_id must be between 1 and 254")
|
|
|
|
raise typer.Exit(code=1)
|
|
|
|
with logger.contextualize(syd_id=sys_id):
|
|
|
|
env = os.environ
|
|
|
|
with modified_environ(
|
2024-11-04 17:29:16 -04:00
|
|
|
SERIAL0_PORT=str(int(env["SERIAL0_PORT"]) + 10 * instance),
|
|
|
|
MAVROS2_PORT=str(int(env["MAVROS2_PORT"]) + 10 * instance),
|
|
|
|
MAVROS1_PORT=str(int(env["MAVROS1_PORT"]) + 10 * instance),
|
|
|
|
FDM_PORT_IN=str(int(env["FDM_PORT_IN"]) + 10 * instance),
|
|
|
|
SITL_PORT=str(int(env["SITL_PORT"]) + 10 * instance),
|
|
|
|
INSTANCE=str(instance),
|
2024-10-25 14:49:07 -03:00
|
|
|
DRONE_SYS_ID=str(sys_id),
|
2024-11-01 12:20:39 -03:00
|
|
|
):
|
2024-10-25 14:49:07 -03:00
|
|
|
logger.info("Starting drone stack, this may take some time")
|
|
|
|
docker_stack = sh.docker.compose(
|
|
|
|
"--profile",
|
|
|
|
"uav-sim",
|
2024-11-01 15:32:43 -03:00
|
|
|
"-p",
|
2024-11-06 12:58:14 -04:00
|
|
|
f"robot-sim-{sys_id}",
|
2024-10-25 14:49:07 -03:00
|
|
|
"up",
|
|
|
|
_out=outputLogger("docker_stack", sys_id),
|
|
|
|
_err=sys.stderr,
|
|
|
|
_bg=True,
|
|
|
|
)
|
|
|
|
processes.append(docker_stack)
|
|
|
|
|
|
|
|
|
|
|
|
@app.command()
|
|
|
|
def start_group():
|
|
|
|
env = os.environ
|
2024-11-01 12:20:39 -03:00
|
|
|
sim_drone_count = int(env["SIM_DRONE_COUNT"])
|
2024-10-29 17:00:57 -03:00
|
|
|
start_ros_master()
|
2024-10-25 14:49:07 -03:00
|
|
|
"""Start a group of robots"""
|
|
|
|
for i in range(sim_drone_count):
|
|
|
|
logger.info(f"start robot {i}")
|
2024-11-04 17:29:16 -04:00
|
|
|
start(instance=i, sys_id=i + 1)
|
2024-10-25 14:49:07 -03:00
|
|
|
# if i == 0:
|
|
|
|
# wait_for_gazebo()
|
|
|
|
|
2024-11-01 12:20:39 -03:00
|
|
|
|
2024-10-29 17:00:57 -03:00
|
|
|
def start_ros_master():
|
|
|
|
docker_stack = sh.docker.compose(
|
|
|
|
"--profile",
|
|
|
|
"ros-master",
|
|
|
|
"up",
|
|
|
|
_out=outputLogger("docker_stack", "ros-master"),
|
|
|
|
_err=sys.stderr,
|
|
|
|
_bg=True,
|
|
|
|
)
|
|
|
|
processes.append(docker_stack)
|
2024-10-25 14:49:07 -03:00
|
|
|
|
2024-11-01 12:20:39 -03:00
|
|
|
|
2024-10-25 14:49:07 -03:00
|
|
|
def cleanup():
|
|
|
|
# Wait for all subprocesses to exit
|
|
|
|
logger.info("Waiting for commands to exit")
|
2024-10-29 17:00:57 -03:00
|
|
|
try:
|
|
|
|
if processes:
|
|
|
|
print(processes)
|
|
|
|
for waitable in processes:
|
|
|
|
waitable.kill()
|
|
|
|
waitable.wait()
|
2024-11-01 12:20:39 -03:00
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
|
2024-10-25 14:49:07 -03:00
|
|
|
|
|
|
|
atexit.register(cleanup)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
try:
|
|
|
|
app()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
logger.info("KeyboardInterrupt caught, exiting...")
|
|
|
|
cleanup()
|
|
|
|
sys.exit(0)
|