| name | robot-bringup |
| description | Configures production ROS2 boot on Linux: systemd units, layered launch, udev stable device names, CycloneDDS/FastDDS discovery, watchdogs, and graceful actuator shutdown. Use when bringing up a robot stack, writing service units, or debugging boot-time races. Not for Nav2/SLAM algorithms, Gazebo simulation, or Windows-native ROS. |
| version | 1.0.1 |
| risk | safe |
| source | modernized |
| date_modernized | 2026-05-31 |
| tags | ["robotics","ros2","systemd","dds","deployment","linux"] |
| tools | ["bash","ssh"] |
When to Use
Use this skill when the user needs to:
- Configure a robot to automatically start its full ROS2 stack on boot via systemd
- Write systemd unit files that correctly source ROS2 workspaces and set DDS environment
- Compose layered launch files (hardware, drivers, perception, application) into a single bringup
- Set up ordered startup with health checks to avoid race conditions between dependent nodes
- Write udev rules for deterministic device naming of cameras, LiDARs, and serial devices
- Configure CycloneDDS or FastDDS for multi-machine ROS2 discovery across robot and base station
- Implement watchdog and heartbeat monitoring for production robot systems
- Set up log rotation and structured logging for long-running robot deployments
- Write graceful shutdown handlers that bring actuators to a safe state before exit
- Debug boot-time failures, service ordering issues, or device enumeration races
Trigger keywords: systemd, ros2 launch, bringup, udev, cyclonedds, fastdds, robot boot, service unit, watchdog, ROS_DOMAIN_ID, rmw
Prerequisites
- A Linux robot computer running systemd (Ubuntu 22.04+ or equivalent)
- ROS2 installed at
/opt/ros/${ROS_DISTRO}/ (tested with Humble and Jazzy)
- A colcon workspace at
/home/robot/ros2_ws/ with the robot's packages built
- Root or sudo access for installing systemd units and udev rules
- A dedicated
robot system user (not root) for running the stack
- Network connectivity between robot and base station if multi-machine DDS is needed
Create the robot user
sudo useradd -r -m -s /bin/bash robot
sudo usermod -aG dialout,video,plugdev robot
Where the recipes live
This skill does not ship a companion pack. Use the Procedure sections in this file:
- udev rules for USB serial, cameras, LiDARs — Procedure 2
- CycloneDDS / FastDDS XML and multi-machine discovery — Procedure 4
- systemd hardening (
ProtectSystem, PrivateTmp, capability bounding) — Procedure 6f
ExecStartPre device verification — Procedure 5 (install as /usr/local/bin/robot-device-check.sh)
- deploy from a dev machine to the robot — Procedure 10
Procedure
1. The Robot Bringup Stack
A production robot bringup follows a layered startup sequence from hardware initialization through application-level nodes. Each layer depends on the one below it.
APPLICATION LAYER
Navigation, manipulation, mission planning, HRI
PERCEPTION LAYER
Object detection, SLAM, point cloud filtering, sensor fusion
DRIVER LAYER
Camera drivers, LiDAR drivers, motor controllers, IMU
HARDWARE LAYER
udev rules, device enumeration, USB reset, firmware check
ROS2 ENVIRONMENT
Source workspace, set RMW, ROS_DOMAIN_ID, DDS config
SYSTEMD TARGETS & SERVICES
network-online.target robot-hw.target robot-bringup.target
LINUX BOOT (systemd)
BIOS/UEFI GRUB kernel systemd init
HARDWARE BOOT
Power supply, onboard computer, peripherals
2. Write udev rules for deterministic device naming
Hardcoded paths like /dev/ttyUSB0 are unreliable because USB enumeration order changes between reboots. Create stable symlinks under /dev/robot/.
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="0001", SYMLINK+="robot/lidar", MODE="0666", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="0002", SYMLINK+="robot/imu", MODE="0666", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="ABCD1234", SYMLINK+="robot/motors", MODE="0666", GROUP="dialout"
SUBSYSTEM=="video4linux", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0825", ATTRS{serial}=="A1B2C3D4", SYMLINK+="robot/camera_front", MODE="0666", GROUP="video"
Apply and test:
sudo udevadm control --reload-rules
sudo udevadm trigger
ls -la /dev/robot/
3. Create the ROS2 environment file
systemd does not load ~/.bashrc or ~/.profile. Store environment variables in a dedicated file.
ROS_DISTRO=humble
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ROS_DOMAIN_ID=42
CYCLONEDDS_URI=file:///etc/robot/cyclonedds.xml
ROS_LOCALHOST_ONLY=0
ROS_LOG_DIR=/var/log/ros2
RCUTILS_LOGGING_USE_STDOUT=0
RCUTILS_COLORIZED_OUTPUT=0
ROBOT_NAME=my_robot_01
ROBOT_CONFIG_DIR=/etc/robot/config
sudo mkdir -p /etc/robot
sudo nano /etc/robot/ros2.env
sudo mkdir -p /var/log/ros2
sudo chown robot:robot /var/log/ros2
4. Configure CycloneDDS for multi-machine discovery
<?xml version="1.0" encoding="UTF-8" ?>
<CycloneDDS xmlns="https://cdds.io/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://cdds.io/config https://raw.githubusercontent.com/eclipse-cyclonedds/cyclonedds/master/etc/cyclonedds.xsd">
<Domain id="any">
<General>
<Interfaces>
<NetworkInterface name="eth0" />
</Interfaces>
</General>
<Discovery>
<Peers>
<Peer address="192.168.1.100"/>
<Peer address="192.168.1.101"/>
</Peers>
<ParticipantIndex>auto</ParticipantIndex>
</Discovery>
</Domain>
</CycloneDDS>
5. Write the device check script
Write this ExecStartPre checker and install it as /usr/local/bin/robot-device-check.sh:
#!/usr/bin/env bash
set -euo pipefail
DEVICES=(
"/dev/robot/camera_front"
"/dev/robot/lidar"
"/dev/robot/imu"
"/dev/robot/motors"
)
for dev in "${DEVICES[@]}"; do
if [[ ! -e "$dev" ]]; then
echo "ERROR: Missing device $dev" >&2
exit 1
fi
echo "OK: $dev present"
done
echo "All critical devices ready."
exit 0
sudo chmod +x /usr/local/bin/robot-device-check.sh
6. Write systemd service units
Place service files in /etc/systemd/system/. Split the stack into multiple services with explicit ordering for independent restart and failure isolation.
6a. Hardware target
[Unit]
Description=Robot Hardware Ready (udev devices enumerated)
After=network-online.target
Wants=network-online.target
[Install]
WantedBy=multi-user.target
6b. Driver service
[Unit]
Description=Robot Hardware Drivers (cameras, LiDAR, IMU, motors)
After=network-online.target robot-hw.target
Wants=network-online.target
Requires=robot-hw.target
[Service]
Type=notify
User=robot
Group=robot
EnvironmentFile=/etc/robot/ros2.env
ExecStartPre=/usr/local/bin/robot-device-check.sh
ExecStart=/bin/bash -c '\
source /opt/ros/${ROS_DISTRO}/setup.bash && \
source /home/robot/ros2_ws/install/setup.bash && \
exec ros2 launch my_robot_bringup drivers.launch.py'
ExecStop=/bin/kill -INT $MAINPID
TimeoutStopSec=20
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=120
StartLimitBurst=5
WatchdogSec=30
KillMode=mixed
KillSignal=SIGINT
FinalKillSignal=SIGKILL
TimeoutStartSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=robot-drivers
OnFailure=robot-alert@%n.service
[Install]
WantedBy=robot-bringup.target
6c. Perception service
[Unit]
Description=Robot Perception Stack (SLAM, detection, sensor fusion)
After=robot-drivers.service
Requires=robot-drivers.service
PartOf=robot-drivers.service
[Service]
Type=notify
User=robot
Group=robot
EnvironmentFile=/etc/robot/ros2.env
ExecStart=/bin/bash -c '\
source /opt/ros/${ROS_DISTRO}/setup.bash && \
source /home/robot/ros2_ws/install/setup.bash && \
exec ros2 launch my_robot_bringup perception.launch.py'
ExecStop=/bin/kill -INT $MAINPID
TimeoutStopSec=20
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=120
StartLimitBurst=5
WatchdogSec=30
KillMode=mixed
KillSignal=SIGINT
FinalKillSignal=SIGKILL
StandardOutput=journal
StandardError=journal
SyslogIdentifier=robot-perception
OnFailure=robot-alert@%n.service
[Install]
WantedBy=robot-bringup.target
6d. Application service
[Unit]
Description=Robot Application Layer (navigation, planning, HRI)
After=robot-perception.service
Requires=robot-perception.service
PartOf=robot-perception.service
[Service]
Type=notify
User=robot
Group=robot
EnvironmentFile=/etc/robot/ros2.env
ExecStart=/bin/bash -c '\
source /opt/ros/${ROS_DISTRO}/setup.bash && \
source /home/robot/ros2_ws/install/setup.bash && \
exec ros2 launch my_robot_bringup application.launch.py'
ExecStop=/bin/kill -INT $MAINPID
TimeoutStopSec=20
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=120
StartLimitBurst=5
WatchdogSec=30
KillMode=mixed
KillSignal=SIGINT
FinalKillSignal=SIGKILL
StandardOutput=journal
StandardError=journal
SyslogIdentifier=robot-application
OnFailure=robot-alert@%n.service
[Install]
WantedBy=robot-bringup.target
6e. Bringup target (composes all layers)
[Unit]
Description=Robot Full Bringup Stack
After=network-online.target
Wants=network-online.target
[Install]
WantedBy=multi-user.target
6f. Resource limits and hardening (optional drop-in)
[Service]
MemoryMax=2G
MemoryHigh=1800M
CPUQuota=300%
Nice=-5
IOSchedulingClass=realtime
IOSchedulingPriority=0
ProtectHome=read-only
ProtectSystem=strict
ReadWritePaths=/var/log/ros2 /tmp
PrivateTmp=true
7. Write layered launch files
Organize launch files into layers that mirror the systemd service architecture. Each layer is independently testable.
bringup.launch.py (top-level: composes all layers)
hardware.launch.py (udev checks, device readiness)
drivers.launch.py (camera, LiDAR, IMU, motor drivers)
camera.launch.py
lidar.launch.py
motors.launch.py
perception.launch.py (SLAM, detection, fusion)
slam.launch.py
detection.launch.py
application.launch.py (navigation, planning, HRI)
navigation.launch.py
mission.launch.py
7a. Hardware layer launch
from launch import LaunchDescription
from launch.actions import LogInfo, ExecuteProcess, TimerAction
from launch.substitutions import LaunchConfiguration, EnvironmentVariable
def generate_launch_description():
robot_name = LaunchConfiguration('robot_name',
default=EnvironmentVariable('ROBOT_NAME', default_value='default_robot'))
check_camera = ExecuteProcess(
cmd=['test', '-e', '/dev/robot/camera_front'],
name='check_camera_front',
output='screen',
)
check_lidar = ExecuteProcess(
cmd=['test', '-e', '/dev/robot/lidar'],
name='check_lidar',
output='screen',
)
check_imu = ExecuteProcess(
cmd=['test', '-e', '/dev/robot/imu'],
name='check_imu',
output='screen',
)
log_ready = TimerAction(
period=2.0,
actions=[LogInfo(msg='Hardware checks passed, devices ready')],
)
return LaunchDescription([
check_camera,
check_lidar,
check_imu,
log_ready,
])
7b. Driver layer launch
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
camera_node = Node(
package='usb_cam',
executable='usb_cam_node_exe',
name='camera_front',
parameters=[PathJoinSubstitution([
FindPackageShare('my_robot_bringup'), 'config', 'camera_front.yaml'
])],
remappings=[('/image_raw', '/camera/front/image_raw')],
)
lidar_node = Node(
package='sllidar_ros2',
executable='sllidar_node',
name='lidar',
parameters=[{
'serial_port': '/dev/robot/lidar',
'serial_baudrate': 460800,
'frame_id': 'lidar_link',
'angle_compensate': True,
}],
)
imu_node = Node(
package='imu_driver',
executable='imu_node',
name='imu',
parameters=[{
'port': '/dev/robot/imu',
'frame_id': 'imu_link',
'publish_rate': 100.0,
}],
)
motor_node = Node(
package=,
executable=,
name=,
parameters=[PathJoinSubstitution([
FindPackageShare(), ,
])],
)
LaunchDescription([
DeclareLaunchArgument(, default_value=),
DeclareLaunchArgument(, default_value=),
camera_node,
lidar_node,
imu_node,
motor_node,
])
7c. Perception layer launch
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node, ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
enable_slam = LaunchConfiguration('enable_slam', default='true')
enable_detection = LaunchConfiguration('enable_detection', default='true')
perception_container = ComposableNodeContainer(
name='perception_container',
namespace='',
package='rclcpp_components',
executable='component_container_mt',
composable_node_descriptions=[
ComposableNode(
package='image_proc',
plugin='image_proc::RectifyNode',
name='rectify',
remappings=[('image', '/camera/front/image_raw')],
),
ComposableNode(
package='my_detection',
plugin='my_detection::DetectorNode',
name='detector',
parameters=[PathJoinSubstitution([
FindPackageShare('my_robot_bringup'), 'config', 'detector.yaml'
])],
),
],
condition=IfCondition(enable_detection),
)
slam_node = Node(
package=,
executable=,
name=,
parameters=[PathJoinSubstitution([
FindPackageShare(), ,
])],
condition=IfCondition(enable_slam),
)
LaunchDescription([
DeclareLaunchArgument(, default_value=),
DeclareLaunchArgument(, default_value=),
perception_container,
slam_node,
])
7d. Application layer launch
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
nav_params = LaunchConfiguration('nav_params', default=PathJoinSubstitution([
FindPackageShare('my_robot_bringup'), 'config', 'nav2_params.yaml'
]))
nav2_bringup = IncludeLaunchDescription(
PythonLaunchDescriptionSource(PathJoinSubstitution([
FindPackageShare('nav2_bringup'), 'launch', 'bringup_launch.py'
])),
launch_arguments={
'params_file': nav_params,
'use_sim_time': LaunchConfiguration('use_sim', default='false'),
}.items(),
)
mission_node = Node(
package='my_mission',
executable='mission_planner',
name='mission_planner',
parameters=[PathJoinSubstitution([
FindPackageShare('my_robot_bringup'), 'config', 'mission.yaml'
])],
)
return LaunchDescription([
DeclareLaunchArgument('nav_params', default_value=''),
DeclareLaunchArgument('use_sim', default_value='false'),
nav2_bringup,
mission_node,
])
7e. Top-level bringup launch
from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument, IncludeLaunchDescription,
GroupAction, LogInfo, TimerAction,
)
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import (
LaunchConfiguration, PathJoinSubstitution, PythonExpression,
)
from launch_ros.actions import PushRosNamespace
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
pkg_share = FindPackageShare('my_robot_bringup')
use_sim = LaunchConfiguration('use_sim')
hardware_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([pkg_share, 'launch', 'hardware.launch.py'])
),
condition=UnlessCondition(use_sim),
)
drivers_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([pkg_share, 'launch', 'drivers.launch.py'])
),
condition=UnlessCondition(use_sim),
)
perception_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([pkg_share, 'launch', 'perception.launch.py'])
),
)
application_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([pkg_share, 'launch', 'application.launch.py'])
),
)
return LaunchDescription([
DeclareLaunchArgument('use_sim', default_value='false'),
hardware_launch,
TimerAction(period=3.0, actions=[drivers_launch]),
TimerAction(period=, actions=[perception_launch]),
TimerAction(period=, actions=[application_launch]),
])
8. Implement graceful shutdown handlers
All actuator nodes must command a safe state before exit. Register signal handlers or use rclpy's shutdown callback.
def main():
rclpy.init()
node = MotorControlNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.command_zero_velocity()
node.engage_brakes()
node.destroy_node()
rclpy.shutdown()
9. Configure log rotation
/var/log/ros2/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0644 robot robot
}
SystemMaxUse=1G
MaxFileSec=1month
sudo systemctl restart systemd-journald
10. Deploy from dev machine to robot
Deploy with this workflow (save it in the project if you want to reuse it):
#!/usr/bin/env bash
set -euo pipefail
ROBOT_HOST="robot@192.168.1.101"
WORKSPACE="/home/robot/ros2_ws"
ROS_DISTRO="humble"
echo "=== Building bringup package locally ==="
source /opt/ros/${ROS_DISTRO}/setup.bash
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release --packages-select my_robot_bringup
echo "=== Syncing to robot ==="
rsync -avz --delete \
--exclude='build/' --exclude='log/' \
src/ "${ROBOT_HOST}:${WORKSPACE}/src/"
echo "=== Building on robot ==="
ssh "$ROBOT_HOST" "cd ${WORKSPACE} && \
source /opt/ros/\${ROS_DISTRO}/setup.bash && \
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release"
echo "=== Restarting robot services ==="
ssh "$ROBOT_HOST" 'sudo systemctl restart robot-bringup.target'
echo "=== Checking service status ==="
ssh "$ROBOT_HOST" 'sleep 3 && systemctl status robot-bringup.target --no-pager'
echo "Deploy complete."
11. Enable and start services
sudo systemctl daemon-reload
sudo systemctl enable robot-hw.target robot-bringup.target
sudo systemctl enable robot-drivers.service robot-perception.service robot-application.service
sudo systemctl start robot-bringup.target
Pitfalls
1. Sourcing setup.bash in .bashrc for systemd
systemd services do not load ~/.bashrc or ~/.profile. Environment variables set there are invisible to the service, causing "command not found" or missing package errors. Always use EnvironmentFile=/etc/robot/ros2.env in the service unit and source explicitly in ExecStart.
2. No startup ordering
Starting all ROS2 nodes simultaneously causes race conditions. A navigation node may attempt to call a service that has not yet been advertised by the driver. Always use After= and Requires= in systemd units, or use a lifecycle manager to enforce ordered transitions.
3. Using Restart=always without rate limiting
A service that crashes on startup (missing config, hardware disconnected) will restart in a tight loop, consuming CPU and flooding the journal. Always set StartLimitIntervalSec=120 and StartLimitBurst=5 alongside Restart=on-failure.
4. Relying on network.target instead of network-online.target
network.target is reached as soon as network configuration starts, not when connectivity is established. DDS discovery fails because the network interface has no IP yet. Always use After=network-online.target and Wants=network-online.target. Ensure systemd-networkd-wait-online.service or NetworkManager-wait-online.service is enabled.
5. No log rotation
ROS2 log files in ~/.ros/log/ and journal entries grow without limit, eventually filling the disk on embedded systems. Configure logrotate for $ROS_LOG_DIR and set SystemMaxUse=1G in journald.conf.
6. Hardcoded device paths (/dev/ttyUSB0)
/dev/ttyUSB0 can be assigned to any USB serial device depending on enumeration order. After a reboot, the IMU might become /dev/ttyUSB1 and the motor controller /dev/ttyUSB0, reversing the mapping. Always use udev rules to create stable symlinks under /dev/robot/.
7. Running the entire stack as root
Running ROS2 as root is a security risk and causes permission issues with rosbag2, log files, and parameter persistence. Create a dedicated robot user and grant only necessary device permissions via udev GROUP and MODE rules. Set User=robot and Group=robot in every service unit.
8. No graceful shutdown handler
When systemd sends SIGTERM or SIGINT, a node without a shutdown handler exits immediately without commanding zero velocity or engaging brakes. The robot may coast or continue moving with the last commanded velocity. Always register signal handlers that command a safe state in the finally block before rclpy.shutdown().
9. WatchdogSec without sd_notify in the node
Setting WatchdogSec=30 in the service unit requires the ROS2 process to call sd_notify(WATCHDOG=1) within that interval. If the node does not implement this, systemd will kill and restart it repeatedly. Either implement sd_notify in the node or remove WatchdogSec and rely on Restart=on-failure alone.
10. PartOf without Requires
PartOf= creates a stop/restart dependency but does not create a start dependency. If you want a service to start when its parent starts, you also need Requires= (or Wants= for optional). Using only PartOf= means stopping the parent stops the child, but starting the parent does not start the child.
Verification
Verify udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger
ls -la /dev/robot/
Verify environment file
systemd-analyze verify /etc/robot/ros2.env 2>&1 || true
sudo systemctl show robot-drivers.service -p Environment
Verify service unit syntax
sudo systemd-analyze verify /etc/systemd/system/robot-*.service /etc/systemd/system/robot-*.target
Verify service ordering
systemctl list-dependencies robot-bringup.target
Verify services are running
sudo systemctl status robot-bringup.target
sudo systemctl status robot-drivers.service
sudo systemctl status robot-perception.service
sudo systemctl status robot-application.service
Verify ROS2 topics are publishing
source /opt/ros/humble/setup.bash
source /home/robot/ros2_ws/install/setup.bash
export ROS_DOMAIN_ID=42
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 topic list
ros2 topic hz /scan
ros2 node list
Verify DDS discovery across machines
ros2 daemon start
ros2 node list
Verify graceful shutdown
sudo systemctl stop robot-application.service
journalctl -u robot-application.service -n 20 --no-pager
Verify boot-time startup (full integration test)
sudo reboot
systemctl is-active robot-bringup.target
systemctl is-active robot-drivers.service robot-perception.service robot-application.service
systemctl --failed
Verify log rotation is working
journalctl --disk-usage
sudo logrotate -d /etc/logrotate.d/ros2
Robot Bringup Checklist
- udev rules written and tested for all USB devices with stable symlinks under
/dev/robot/
- systemd service units created for each layer with correct
After=/Requires= ordering
- ROS2 environment file (
/etc/robot/ros2.env) configured with ROS_DISTRO, RMW_IMPLEMENTATION, ROS_DOMAIN_ID, and CYCLONEDDS_URI
- CycloneDDS or FastDDS XML configured with explicit peer list for multi-machine discovery
- Launch files layered and composable with conditional arguments for sim/real and robot variants
- Health check scripts written for
ExecStartPre to verify device presence before starting drivers
- Watchdog integration configured:
WatchdogSec in service units and sd_notify(WATCHDOG=1) in the ROS2 process
- Heartbeat monitor node deployed to detect node failures and trigger safe stop
- Graceful shutdown handlers registered in all actuator nodes (zero velocity, engage brakes on
SIGINT/SIGTERM)
- Log rotation configured via logrotate for
$ROS_LOG_DIR and journald SystemMaxUse limits
- Restart policies rate-limited with
StartLimitIntervalSec and StartLimitBurst to prevent restart loops
- Resource limits set via
MemoryMax, CPUQuota to prevent runaway nodes from starving the system
- Network and firewall configured with static IPs, DDS port rules, and
ROS_LOCALHOST_ONLY set correctly
- Full boot test performed from power-off to autonomous operation, verifying service ordering and recovery from simulated failures
Related Skills
ros2-launch — Writing and debugging ROS2 launch files
dds-config — CycloneDDS and FastDDS configuration for multi-robot fleets
systemd-services — General systemd service unit authoring and hardening
robot-safety — Safety interlocks, e-stops, and watchdog patterns for production robots