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
36 changes: 29 additions & 7 deletions bridge/bridge_node/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,33 @@ def _on_msg(self, msg: BatteryState) -> None:
self.fanout.push(ev)


def serve(socket_path: str) -> None:
def serve(addr: str) -> None:
"""Run the bridge gRPC server.

`addr` accepts either a bare ``host:port`` for TCP, or a path /
``unix://<path>`` form for a Unix domain socket. Determined by
whether the value contains a colon and no slash.
"""
fanout = TopicFanout()

server = grpc.server(futures.ThreadPoolExecutor(max_workers=8))
pbg.add_BridgeServicer_to_server(BridgeService(fanout), server)

if os.path.exists(socket_path):
os.unlink(socket_path)
server.add_insecure_port(f"unix://{socket_path}")
if addr.startswith("unix://"):
listen = addr
sock_path = addr[len("unix://"):]
if os.path.exists(sock_path):
os.unlink(sock_path)
elif ":" in addr and "/" not in addr:
listen = addr # bare host:port -> TCP
else:
# bare path -> Unix domain socket
if os.path.exists(addr):
os.unlink(addr)
listen = f"unix://{addr}"
server.add_insecure_port(listen)
server.start()
log.info("bridge listening on %s", socket_path)
log.info("bridge listening on %s", listen)

if HAVE_ROS:
rclpy.init(args=None)
Expand All @@ -163,10 +179,16 @@ def serve(socket_path: str) -> None:

def main() -> None: # pragma: no cover
p = argparse.ArgumentParser()
p.add_argument("--socket", default="/run/temporal-hack-bridge.sock")
# --listen is the modern arg (TCP host:port or unix:// path).
# --socket kept for backwards compat with older callers.
p.add_argument("--listen", default=None,
help="gRPC listen address: 'host:port' or 'unix:///path'")
p.add_argument("--socket", default=None,
help="(deprecated) Unix domain socket path. Use --listen.")
args = p.parse_args()
addr = args.listen or args.socket or "/run/temporal-hack-bridge.sock"
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
serve(args.socket)
serve(addr)


if __name__ == "__main__":
Expand Down
53 changes: 53 additions & 0 deletions docker/gazebo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Gazebo (simulator) container.
#
# Runs Ignition Fortress + ros_gz_bridge + an Xvfb/x11vnc/noVNC stack
# for browser-served GUI access. NOTHING ROS-application-specific
# lives here — the rover's controllers (drive-circle, drive-figure-eight)
# and the agent's bridge_node live in their own containers and reach
# this one over the lab network's ROS DDS domain.
#
# What's in vs. out:
# IN: gz sim, ros_gz_bridge, Xvfb/x11vnc/novnc/fluxbox, mesa,
# worlds/, models/
# OUT: bridge_node (rclpy → agent gRPC), sim_battery, controllers
FROM ros:humble-ros-base

ENV DEBIAN_FRONTEND=noninteractive
ENV ROS_DOMAIN_ID=42
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

RUN apt-get update && apt-get install -y --no-install-recommends \
wget gnupg lsb-release ca-certificates \
&& wget -qO /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg \
https://packages.osrfoundation.org/gazebo.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" \
> /etc/apt/sources.list.d/gazebo-stable.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ros-humble-rmw-cyclonedds-cpp \
ros-humble-ros-gz-bridge \
ignition-fortress \
libgl1-mesa-dri \
libgl1-mesa-glx \
mesa-utils \
xvfb \
x11vnc \
novnc \
websockify \
fluxbox \
net-tools \
&& rm -rf /var/lib/apt/lists/*

# Sim worlds + models. GZ_SIM_RESOURCE_PATH lets gz-sim resolve
# `model://...` includes at runtime.
COPY docker/gazebo/worlds /opt/sim/worlds
COPY docker/gazebo/models /opt/sim/models
ENV GZ_SIM_RESOURCE_PATH="/opt/sim/models:/opt/sim/worlds"

COPY docker/gazebo/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# noVNC (browser GUI): http://localhost:14680
# Raw VNC (any client): localhost:14900
EXPOSE 5900 6080
ENTRYPOINT ["/entrypoint.sh"]
92 changes: 92 additions & 0 deletions docker/gazebo/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# gazebo container entrypoint:
# Xvfb -> fluxbox -> x11vnc -> websockify (browser GUI pipe)
# ign gazebo -r -v 4 <world> (sim engine + GUI)
# ros_gz_bridge (subscribes to /cmd_vel
# on ROS DDS, forwards to
# the gz cmd_vel topic)
set -eo pipefail

# ROS 2's setup.bash references vars under nounset; wrap.
set +u; source /opt/ros/humble/setup.bash; set -u

SIM_WORLD="${SIM_WORLD:-moon.sdf}"
HEADLESS="${HEADLESS:-0}"
DISPLAY_NUM="${DISPLAY_NUM:-1}"
SCREEN_GEOMETRY="${SCREEN_GEOMETRY:-1920x1200x24}"

PIDS=()
shutdown() {
echo "[gazebo] shutting down"
for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null || true; done
exit 0
}
trap shutdown SIGINT SIGTERM

if [ "$HEADLESS" = "1" ]; then
echo "[gazebo] HEADLESS=1; running ign gazebo server only ($SIM_WORLD)"
ign gazebo -s -r -v 4 "$SIM_WORLD" &
PIDS+=($!)
else
echo "[gazebo] starting Xvfb on :$DISPLAY_NUM ($SCREEN_GEOMETRY)"
Xvfb ":$DISPLAY_NUM" -screen 0 "$SCREEN_GEOMETRY" -nolisten tcp &
PIDS+=($!)
sleep 1
export DISPLAY=":$DISPLAY_NUM"

fluxbox 2>/dev/null &
PIDS+=($!)

x11vnc -display "$DISPLAY" -forever -shared -nopw -quiet -rfbport 5900 -bg -o /tmp/x11vnc.log

websockify --web=/usr/share/novnc 6080 localhost:5900 &
PIDS+=($!)

echo "[gazebo] launching ign gazebo with GUI: $SIM_WORLD"
export LIBGL_ALWAYS_SOFTWARE=1
export OGRE2_RTSHADERSYSTEM_WRITE_SHADERS_TO_DISK=0
ign gazebo -r -v 4 "$SIM_WORLD" &
PIDS+=($!)

echo
echo " ┌──────────────────────────────────────────────────────────────────────────────┐"
echo " │ Gazebo GUI: http://localhost:14680/vnc.html?autoconnect=1&resize=scale │"
echo " │ Raw VNC: localhost:14900 (no password) │"
echo " └──────────────────────────────────────────────────────────────────────────────┘"
echo
fi

# Bridge ROS 2 /cmd_vel and odom for the perseverance rover. The
# diff-drive plugin in moon.sdf listens on /model/perseverance/cmd_vel;
# remap so external ROS 2 controllers can publish to plain /cmd_vel.
sleep 4
case "$SIM_WORLD" in
moon.sdf)
echo "[gazebo] starting ros_gz_bridge: /cmd_vel + odom + /perseverance/contacts"
ros2 run ros_gz_bridge parameter_bridge \
/model/perseverance/cmd_vel@geometry_msgs/msg/Twist@ignition.msgs.Twist \
/model/perseverance/odometry@nav_msgs/msg/Odometry@ignition.msgs.Odometry \
/perseverance/contacts@ros_gz_interfaces/msg/Contacts@ignition.msgs.Contacts \
--ros-args \
-r /model/perseverance/cmd_vel:=/cmd_vel \
-r /model/perseverance/odometry:=/odom \
-r /perseverance/contacts:=/contacts &
PIDS+=($!)
;;
diff_drive.sdf|diff_drive_skid.sdf)
echo "[gazebo] starting ros_gz_bridge: /cmd_vel ↔ /model/vehicle_blue/cmd_vel"
ros2 run ros_gz_bridge parameter_bridge \
/model/vehicle_blue/cmd_vel@geometry_msgs/msg/Twist@ignition.msgs.Twist \
/model/vehicle_blue/odometry@nav_msgs/msg/Odometry@ignition.msgs.Odometry \
--ros-args \
-r /model/vehicle_blue/cmd_vel:=/cmd_vel \
-r /model/vehicle_blue/odometry:=/odom &
PIDS+=($!)
;;
*)
echo "[gazebo] no ros_gz_bridge wired for world '$SIM_WORLD'"
;;
esac

# Wait for any background process to exit (or signal).
wait -n
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
<collision name="deck_col">
<geometry><box><size>1.20 1.40 0.40</size></box></geometry>
</collision>

<!-- Contact sensor on the body. Pairs with the gz-sim contact
system plugin in moon.sdf; emits an ignition.msgs.Contacts
message whenever the deck collision touches anything. The
gazebo container's ros_gz_bridge republishes this onto a
ROS 2 topic. -->
<sensor name="bumper" type="contact">
<update_rate>10</update_rate>
<contact>
<collision>deck_col</collision>
<topic>/perseverance/contacts</topic>
</contact>
</sensor>

<visual name="deck">
<geometry><box><size>1.20 1.40 0.40</size></box></geometry>
<material>
Expand Down
21 changes: 21 additions & 0 deletions docker/sim/worlds/moon.sdf → docker/gazebo/worlds/moon.sdf
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@
<material><ambient>0.18 0.17 0.16 1</ambient><diffuse>0.36 0.34 0.32 1</diffuse></material></visual></link>
</model>

<!-- Big obstacle rock placed directly in the rover's forward path.
Rover spawns at origin pointing +x; a boulder at x=8 is hit
if the rover drives forward at any reasonable speed. -->
<model name="boulder">
<static>true</static>
<pose>8 0 0.50 0 0 0</pose>
<link name="link">
<collision name="c">
<geometry><ellipsoid><radii>0.90 0.80 0.50</radii></ellipsoid></geometry>
</collision>
<visual name="v">
<geometry><ellipsoid><radii>0.90 0.80 0.50</radii></ellipsoid></geometry>
<material>
<ambient>0.22 0.20 0.17 1</ambient>
<diffuse>0.45 0.41 0.36 1</diffuse>
<specular>0.05 0.05 0.05 1</specular>
</material>
</visual>
</link>
</model>

<!-- Perseverance rover (model lives at /opt/sim/models/perseverance) -->
<include>
<uri>model://perseverance</uri>
Expand Down
41 changes: 41 additions & 0 deletions docker/robot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Robot container — always-on ROS infrastructure for the simulated
# rover.
#
# What's in vs. out:
# IN: bridge_node (rclpy → gRPC for the agent), sim_battery
# OUT: gz sim, ros_gz_bridge, GUI stack — those live in docker/gazebo/.
# OTA-swappable controllers — those live as separate
# sim/controllers/* images managed by the agent.
#
# This container is *not* the OTA target. It hosts the robot's
# always-on ROS plumbing. The OTA-swappable software runs as a
# sibling container (drive-circle, drive-figure-eight, ...) on the
# same lab network and ROS DDS domain.
FROM ros:humble-ros-base

ENV DEBIAN_FRONTEND=noninteractive
ENV ROS_DOMAIN_ID=42
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ENV PYTHONUNBUFFERED=1

RUN apt-get update && apt-get install -y --no-install-recommends \
ros-humble-rmw-cyclonedds-cpp \
ros-humble-sensor-msgs \
python3-pip \
python3-grpcio \
python3-grpc-tools \
&& rm -rf /var/lib/apt/lists/*

# Bridge package (rclpy → gRPC) and sim_battery publisher.
WORKDIR /opt/bridge
COPY bridge/ /opt/bridge/
RUN python3 -m pip install --no-cache-dir --upgrade "pip>=23.0" \
&& python3 -m pip install --no-cache-dir --break-system-packages /opt/bridge

COPY docker/robot/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# bridge_node gRPC server (TCP) — agent connects here.
EXPOSE 50051

ENTRYPOINT ["/entrypoint.sh"]
27 changes: 27 additions & 0 deletions docker/robot/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# robot container entrypoint:
# sim_battery (rclpy) — synthetic /battery_state publisher
# bridge_node.server (rclpy) — gRPC peer for the agent on TCP 50051
set -eo pipefail

set +u; source /opt/ros/humble/setup.bash; set -u

LISTEN="${BRIDGE_LISTEN:-0.0.0.0:50051}"

PIDS=()
shutdown() {
echo "[robot] shutting down"
for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null || true; done
exit 0
}
trap shutdown SIGINT SIGTERM

# sim_battery is a stand-in /battery_state publisher (Gazebo doesn't
# emit one for our model). Real robots publish their own; this is
# only present in the dev sim.
echo "[robot] starting sim_battery"
python3 -m bridge_node.sim_battery &
PIDS+=($!)

echo "[robot] starting bridge gRPC on $LISTEN"
exec python3 -m bridge_node.server --listen "$LISTEN"
68 changes: 0 additions & 68 deletions docker/sim/Dockerfile

This file was deleted.

Loading