diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c30ae91..5106a87 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,11 +7,7 @@ "runServices": ["sim"], "workspaceFolder": "/home/trickfire/gazebo-simulations", "postCreateCommand": "[ -d .venv ] || (python3 -m venv --system-site-packages .venv && . .venv/bin/activate && pip install -e .)", - // "postStartCommand": "bash ./.devcontainer/x_server.sh", - "mounts": [ - "source=${localEnv:HOME}/.ssh,target=/home/trickfire/.ssh,type=bind,consistency=cached", - "source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,consistency=cached" - ], + "postStartCommand": "bash ./.devcontainer/x_server.sh || exit 0", "forwardPorts": [6080, 5900], "customizations": { "vscode": { diff --git a/cli/cli.py b/cli/cli.py index 7fe09f7..9212213 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -3,14 +3,16 @@ import argparse import shutil import sys +import threading -from .docker import build_and_launch +from .docker import build_and_launch, check_display from .native import build_and_launch_native from .create import create as robot_create, update as robot_update, PACKAGE_DIR, REPO_ROOT from .create.commands import cmd_local, cmd_raw from .auth import auth as cmd_auth from .output import die, info from .paths import WORKSPACE_DIR +from .drpc import rpc_start def main() -> None: @@ -134,8 +136,15 @@ def main() -> None: try: if args.command == "docker": + threading.Thread( + target=rpc_start, kwargs={"is_docker": True, "robot_name": args.robot}, daemon=True + ).start() + check_display() build_and_launch(args.robot, build_only=args.build_only, no_build=args.no_build) elif args.command == "native": + threading.Thread( + target=rpc_start, kwargs={"is_docker": False, "robot_name": args.robot}, daemon=True + ).start() build_and_launch_native(args.robot, build_only=args.build_only, no_build=args.no_build) elif args.command == "clean": for name in ("build", "install", "log"): diff --git a/cli/docker.py b/cli/docker.py index 88dfe75..179221a 100644 --- a/cli/docker.py +++ b/cli/docker.py @@ -114,6 +114,20 @@ def _run_logged_command( raise subprocess.CalledProcessError(return_code, command) +def check_display() -> None: + """Check that a display is available and can be connected to""" + info("Checking for display...") + display = os.environ.copy().get("DISPLAY") + if not display: + die("DISPLAY environment variable not set") + + display_running = subprocess.call( + ["xdpyinfo", "-display", display], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + if display_running != 0: + die("Cannot connect to display " + display) + + def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: bool = False) -> None: """Build the workspace and launch a robot simulation.""" if build_only and no_build: @@ -171,17 +185,6 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo info("Build-only requested; skipping launch") return - info("Checking for display...") - display = env.get("DISPLAY") - if not display: - die("DISPLAY environment variable not set") - - display_running = subprocess.call(["xdpyinfo", "-display", display], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) - if display_running != 0: - die("Cannot connect to display " + display) - info("Sourcing ROS2 environment and launching simulation...") launch_script = f""" set -e diff --git a/cli/drpc.py b/cli/drpc.py new file mode 100644 index 0000000..f2264b4 --- /dev/null +++ b/cli/drpc.py @@ -0,0 +1,49 @@ +"""Discord Rich Presence (DRPC) integration for TrickFire Simulation""" + +import os +import sys +import time + +from pypresence import Presence + + +def _find_discord_socket_dir() -> str | None: + """Return the directory containing a discord-ipc-N socket, or None if not found.""" + candidates = [ + os.environ.get("XDG_RUNTIME_DIR"), # standard Linux user session + os.environ.get("TMPDIR"), + "/run/host-runtime", # devcontainer: host XDG_RUNTIME_DIR mounted here + "/run/user/1000", # fallback for some Linux distros + "/tmp", + ] + for base in filter(None, candidates): + for i in range(10): + if os.path.exists(os.path.join(base, f"discord-ipc-{i}")): + return base + return None + + +def rpc_start(is_docker: bool = False, robot_name: str = "Unknown Robot") -> None: + """Start DRPC in a separate thread""" + try: + if sys.platform == "linux": + socket_dir = _find_discord_socket_dir() + if socket_dir is None: + print("Discord RPC unavailable: no Discord IPC socket found") + return + # pypresence checks TMPDIR early in its path search; point it at the socket dir + # so it works both natively and inside the devcontainer (/run/host-runtime). + os.environ["TMPDIR"] = socket_dir + + client_id = "1504990746527404063" + rpc = Presence(client_id) + rpc.connect() + rpc.update( + name="TrickFire Simulation (" + robot_name + ")", + details="Running TrickFire Simulation on " + robot_name, + state="Running in Docker" if is_docker else "Running locally", + ) + while True: + time.sleep(15) + except Exception as e: # pylint: disable=broad-except + print(f"Discord RPC unavailable: {e}") diff --git a/docker/Dockerfile b/docker/Dockerfile index 8b2a960..9604530 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -102,6 +102,8 @@ RUN printf '#include \nint drmCloseBufferHandle(int fd, unsigned int h # -------------------------------------- # Python packages +RUN pip3 install --break-system-packages \ + pypresence RUN rosdep init || true # -------------------------------------- diff --git a/docker/docker-compose-dev.yml b/docker/docker-compose-dev.yml index af0a469..d003f94 100644 --- a/docker/docker-compose-dev.yml +++ b/docker/docker-compose-dev.yml @@ -4,3 +4,16 @@ services: target: dev args: USER_UID: "${UID:-1000}" + volumes: + - ${XDG_RUNTIME_DIR:-/tmp}:/run/host-runtime:ro + - type: bind + source: ${HOME}/.ssh + target: /home/trickfire/.ssh + read_only: true + bind: + create_host_path: true + - type: bind + source: /tmp/.X11-unix + target: /tmp/.X11-unix + bind: + create_host_path: true diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2cb3ff5..4972d32 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,6 +25,11 @@ services: volumes: - ..:/home/trickfire/gazebo-simulations + - type: bind + source: ${XDG_RUNTIME_DIR:-/run/user/1000} + target: /run/user/1000 + bind: + create_host_path: true working_dir: /home/trickfire/gazebo-simulations diff --git a/pixi.lock b/pixi.lock index 12f74d4..31b074c 100644 --- a/pixi.lock +++ b/pixi.lock @@ -959,6 +959,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/0d/1c817c6769ce0b64cc0dcc20bc9d0edb4dc560e83a3b5f0d5cbda04c3d35/pypresence-4.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/07/86ad9e08fd22d5fbb19f8becfe14195c3388290cc98d58849ef4607f91b8/commentjson-0.8.2.tar.gz - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -1898,6 +1899,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/0d/1c817c6769ce0b64cc0dcc20bc9d0edb4dc560e83a3b5f0d5cbda04c3d35/pypresence-4.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/07/86ad9e08fd22d5fbb19f8becfe14195c3388290cc98d58849ef4607f91b8/commentjson-0.8.2.tar.gz - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -2767,6 +2769,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/0d/1c817c6769ce0b64cc0dcc20bc9d0edb4dc560e83a3b5f0d5cbda04c3d35/pypresence-4.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/07/86ad9e08fd22d5fbb19f8becfe14195c3388290cc98d58849ef4607f91b8/commentjson-0.8.2.tar.gz - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -3635,6 +3638,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/0d/1c817c6769ce0b64cc0dcc20bc9d0edb4dc560e83a3b5f0d5cbda04c3d35/pypresence-4.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/07/86ad9e08fd22d5fbb19f8becfe14195c3388290cc98d58849ef4607f91b8/commentjson-0.8.2.tar.gz - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -62131,6 +62135,22 @@ packages: - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bd/0d/1c817c6769ce0b64cc0dcc20bc9d0edb4dc560e83a3b5f0d5cbda04c3d35/pypresence-4.6.1-py3-none-any.whl + name: pypresence + version: 4.6.1 + sha256: 33d4549fcdf4102f81935df4ba587bacb22193e0c50c541d1ab9329b21df33cd + requires_dist: + - pytest>=7.0.0 ; extra == 'dev' + - pytest-asyncio>=0.21.0 ; extra == 'dev' + - pytest-mock>=3.10.0 ; extra == 'dev' + - pytest-cov>=4.0.0 ; extra == 'dev' + - black>=23.0.0 ; extra == 'dev' + - flake8>=6.0.0 ; extra == 'dev' + - mypy>=1.0.0 ; extra == 'dev' + - isort>=5.12.0 ; extra == 'dev' + - sphinx>=5.0.0 ; extra == 'dev' + - sphinx-rtd-theme>=1.2.0 ; extra == 'dev' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/d0/07/86ad9e08fd22d5fbb19f8becfe14195c3388290cc98d58849ef4607f91b8/commentjson-0.8.2.tar.gz name: commentjson version: 0.8.2 diff --git a/pixi.toml b/pixi.toml index 31b5ee0..6760e11 100644 --- a/pixi.toml +++ b/pixi.toml @@ -20,3 +20,4 @@ scripts = [".devcontainer/pixi.sh"] [pypi-dependencies] sim = { path = ".", editable = true } filelock = "*" +pypresence = "*"