{"id":"dfb02537-ed3d-4347-8017-f284f665678c","shortId":"2uFaVh","kind":"skill","title":"robot-bringup","tagline":"Patterns and best practices for bringing up a complete ROS2-based robotics system on a robot's onboard computer, including systemd services, launch file composition, ordered startup, and production monitoring. Use this skill when configuring a robot to start ROS2 nodes on boot, w","description":"# Robot Bringup Skill\n\n## When to Use This Skill\n\n- Configuring a robot to automatically start its full ROS2 stack on boot via systemd\n- Writing systemd unit files that correctly source ROS2 workspaces and set DDS environment\n- Composing layered launch files (hardware, drivers, perception, application) into a single bringup\n- Setting up ordered startup with health checks to avoid race conditions between dependent nodes\n- Writing udev rules for deterministic device naming of cameras, LiDARs, and serial devices\n- Configuring CycloneDDS or FastDDS for multi-machine ROS2 discovery across robot and base station\n- Implementing watchdog and heartbeat monitoring for production robot systems\n- Setting up log rotation and structured logging for long-running robot deployments\n- Writing graceful shutdown handlers that bring actuators to a safe state before exit\n- Debugging boot-time failures, service ordering issues, or device enumeration races\n\n## The Robot Bringup Stack\n\nA production robot bringup follows a layered startup sequence from hardware initialization through application-level nodes. Each layer depends on the one below it.\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                        APPLICATION LAYER                            │\n│  Navigation, manipulation, mission planning, HRI                    │\n├─────────────────────────────────────────────────────────────────────┤\n│                        PERCEPTION LAYER                             │\n│  Object detection, SLAM, point cloud filtering, sensor fusion       │\n├─────────────────────────────────────────────────────────────────────┤\n│                         DRIVER LAYER                                │\n│  Camera drivers, LiDAR drivers, motor controllers, IMU              │\n├─────────────────────────────────────────────────────────────────────┤\n│                        HARDWARE LAYER                               │\n│  udev rules, device enumeration, USB reset, firmware check          │\n├─────────────────────────────────────────────────────────────────────┤\n│                      ROS2 ENVIRONMENT                               │\n│  Source workspace, set RMW, ROS_DOMAIN_ID, DDS config               │\n├─────────────────────────────────────────────────────────────────────┤\n│                    SYSTEMD TARGETS & SERVICES                       │\n│  network-online.target → robot-hw.target → robot-bringup.target     │\n├─────────────────────────────────────────────────────────────────────┤\n│                      LINUX BOOT (systemd)                           │\n│  BIOS/UEFI → GRUB → kernel → systemd init                          │\n├─────────────────────────────────────────────────────────────────────┤\n│                         HARDWARE BOOT                               │\n│  Power supply, onboard computer, peripherals                        │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n## systemd Service Units for ROS2\n\n### Basic ROS2 Service Unit\n\nPlace service files in `/etc/systemd/system/`. This template starts a ROS2 launch file as a long-running service with watchdog support.\n\n```ini\n# /etc/systemd/system/robot-bringup.service\n[Unit]\nDescription=Robot ROS2 Bringup Stack\nDocumentation=https://github.com/my-org/my-robot\nAfter=network-online.target robot-hw.target\nWants=network-online.target\nRequires=robot-hw.target\n\n[Service]\nType=notify\nUser=robot\nGroup=robot\nWorkingDirectory=/home/robot\n\n# Load ROS2 environment variables from a dedicated env file\nEnvironmentFile=/etc/robot/ros2.env\n\n# Pre-start check: verify critical devices exist\nExecStartPre=/usr/local/bin/robot-device-check.sh\n\n# Start the ROS2 launch file via bash so we can source the workspace\nExecStart=/bin/bash -c '\\\n  source /opt/ros/${ROS_DISTRO}/setup.bash && \\\n  source /home/robot/ros2_ws/install/setup.bash && \\\n  exec ros2 launch my_robot_bringup bringup.launch.py'\n\n# Graceful shutdown: send SIGINT first (Ctrl+C equivalent for ROS2)\nExecStop=/bin/kill -INT $MAINPID\nTimeoutStopSec=30\n\n# Restart on failure, but not on clean exit\nRestart=on-failure\nRestartSec=5\n\n# systemd watchdog: service must call sd_notify(WATCHDOG=1) within this interval\nWatchdogSec=30\n\n# Process management\nKillMode=mixed\nKillSignal=SIGINT\nFinalKillSignal=SIGKILL\nTimeoutStartSec=60\n\n# Logging\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=robot-bringup\n\n[Install]\nWantedBy=multi-user.target\n```\n\n### Environment Setup in systemd\n\nStore environment variables in a dedicated file rather than sourcing .bashrc (which is not loaded by systemd).\n\n```bash\n# /etc/robot/ros2.env\n# ROS2 distribution\nROS_DISTRO=humble\n\n# DDS middleware selection\nRMW_IMPLEMENTATION=rmw_cyclonedds_cpp\n\n# Domain isolation: unique per robot to avoid cross-talk\nROS_DOMAIN_ID=42\n\n# CycloneDDS configuration file path\nCYCLONEDDS_URI=file:///etc/robot/cyclonedds.xml\n\n# Disable localhost-only mode for multi-machine setups\nROS_LOCALHOST_ONLY=0\n\n# Logging configuration\nROS_LOG_DIR=/var/log/ros2\nRCUTILS_LOGGING_USE_STDOUT=0\nRCUTILS_COLORIZED_OUTPUT=0\n\n# Robot-specific configuration\nROBOT_NAME=my_robot_01\nROBOT_CONFIG_DIR=/etc/robot/config\n```\n\n### Dependencies Between Services\n\nSplit the robot stack into multiple systemd services with explicit ordering. This allows independent restart of layers and clearer failure isolation.\n\n```ini\n# /etc/systemd/system/robot-drivers.service\n[Unit]\nDescription=Robot Hardware Drivers (cameras, LiDAR, IMU, motors)\nAfter=network-online.target robot-hw.target\nWants=network-online.target\nRequires=robot-hw.target\n\n[Service]\nType=notify\nUser=robot\nEnvironmentFile=/etc/robot/ros2.env\nExecStart=/bin/bash -c '\\\n  source /opt/ros/${ROS_DISTRO}/setup.bash && \\\n  source /home/robot/ros2_ws/install/setup.bash && \\\n  exec ros2 launch my_robot_bringup drivers.launch.py'\nRestart=on-failure\nRestartSec=5\nWatchdogSec=30\nKillMode=mixed\nKillSignal=SIGINT\nTimeoutStopSec=20\nStandardOutput=journal\nSyslogIdentifier=robot-drivers\n\n[Install]\nWantedBy=robot-bringup.target\n```\n\n```ini\n# /etc/systemd/system/robot-perception.service\n[Unit]\nDescription=Robot Perception Stack (SLAM, detection, sensor fusion)\nAfter=robot-drivers.service\nRequires=robot-drivers.service\nPartOf=robot-drivers.service\n\n[Service]\nType=notify\nUser=robot\nEnvironmentFile=/etc/robot/ros2.env\nExecStart=/bin/bash -c '\\\n  source /opt/ros/${ROS_DISTRO}/setup.bash && \\\n  source /home/robot/ros2_ws/install/setup.bash && \\\n  exec ros2 launch my_robot_bringup perception.launch.py'\nRestart=on-failure\nRestartSec=5\nWatchdogSec=30\nKillMode=mixed\nKillSignal=SIGINT\nTimeoutStopSec=20\nStandardOutput=journal\nSyslogIdentifier=robot-perception\n\n[Install]\nWantedBy=robot-bringup.target\n```\n\n```ini\n# /etc/systemd/system/robot-application.service\n[Unit]\nDescription=Robot Application Layer (navigation, planning, HRI)\nAfter=robot-perception.service\nRequires=robot-perception.service\nPartOf=robot-perception.service\n\n[Service]\nType=notify\nUser=robot\nEnvironmentFile=/etc/robot/ros2.env\nExecStart=/bin/bash -c '\\\n  source /opt/ros/${ROS_DISTRO}/setup.bash && \\\n  source /home/robot/ros2_ws/install/setup.bash && \\\n  exec ros2 launch my_robot_bringup application.launch.py'\nRestart=on-failure\nRestartSec=10\nWatchdogSec=30\nKillMode=mixed\nKillSignal=SIGINT\nTimeoutStopSec=20\nStandardOutput=journal\nSyslogIdentifier=robot-application\n\n[Install]\nWantedBy=robot-bringup.target\n```\n\n### Restart Policies and Failure Recovery\n\nConfigure rate limiting to prevent restart loops when a service is fundamentally broken (e.g., missing device, configuration error).\n\n```ini\n# Add to the [Service] section of any robot service\nRestart=on-failure\nRestartSec=5\n\n# Allow at most 5 restart attempts within 120 seconds\nStartLimitIntervalSec=120\nStartLimitBurst=5\n\n# Ramp up restart delay to avoid thrashing\n# RestartSec can also be set dynamically via drop-in overrides:\n#   RestartSec=5   (first few retries, fast recovery)\n#   After StartLimitBurst is hit, the unit enters failed state\n#   Use systemctl reset-failed robot-drivers.service to retry\n\n# On final failure, trigger an alert\nOnFailure=robot-alert@%n.service\n```\n\n### Resource Limits and cgroups\n\nConstrain resource usage to prevent a runaway node from starving the rest of the system.\n\n```ini\n# Add to the [Service] section\n# Limit memory to 2 GB (hard kill at 2.5 GB)\nMemoryMax=2G\nMemoryHigh=1800M\n\n# Limit CPU to 300% (3 cores on a multi-core system)\nCPUQuota=300%\n\n# Set real-time scheduling priority for time-critical drivers\n# Requires the user to have rtprio permissions in /etc/security/limits.d/\nNice=-5\nIOSchedulingClass=realtime\nIOSchedulingPriority=0\n\n# Restrict filesystem access\nProtectHome=read-only\nProtectSystem=strict\nReadWritePaths=/var/log/ros2 /tmp\nPrivateTmp=true\n```\n\n## Launch File Composition and Layering\n\n### Launch Layer Architecture\n\nOrganize launch files into layers that mirror the systemd service architecture. Each layer is an independent launch file that can be tested in isolation.\n\n```\nbringup.launch.py  (top-level: composes all layers)\n├── hardware.launch.py     (udev checks, device readiness)\n├── drivers.launch.py      (camera, LiDAR, IMU, motor drivers)\n│   ├── camera.launch.py\n│   ├── lidar.launch.py\n│   └── motors.launch.py\n├── perception.launch.py   (SLAM, detection, fusion)\n│   ├── slam.launch.py\n│   └── detection.launch.py\n└── application.launch.py  (navigation, planning, HRI)\n    ├── navigation.launch.py\n    └── mission.launch.py\n```\n\n### Hardware Layer Launch\n\n```python\n# my_robot_bringup/launch/hardware.launch.py\nfrom launch import LaunchDescription\nfrom launch.actions import LogInfo, ExecuteProcess, TimerAction\nfrom launch.conditions import IfCondition\nfrom launch.substitutions import LaunchConfiguration, EnvironmentVariable\n\ndef generate_launch_description():\n    # Declare arguments for hardware configuration\n    robot_name = LaunchConfiguration('robot_name',\n        default=EnvironmentVariable('ROBOT_NAME', default_value='default_robot'))\n\n    # Check that critical devices are present\n    check_camera = ExecuteProcess(\n        cmd=['test', '-e', '/dev/robot/camera_front'],\n        name='check_camera_front',\n        output='screen',\n    )\n\n    check_lidar = ExecuteProcess(\n        cmd=['test', '-e', '/dev/robot/lidar'],\n        name='check_lidar',\n        output='screen',\n    )\n\n    check_imu = ExecuteProcess(\n        cmd=['test', '-e', '/dev/robot/imu'],\n        name='check_imu',\n        output='screen',\n    )\n\n    log_ready = TimerAction(\n        period=2.0,\n        actions=[LogInfo(msg='Hardware checks passed, devices ready')],\n    )\n\n    return LaunchDescription([\n        check_camera,\n        check_lidar,\n        check_imu,\n        log_ready,\n    ])\n```\n\n### Driver Layer Launch\n\n```python\n# my_robot_bringup/launch/drivers.launch.py\nfrom launch import LaunchDescription\nfrom launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, GroupAction\nfrom launch.launch_description_sources import PythonLaunchDescriptionSource\nfrom launch.substitutions import LaunchConfiguration, PathJoinSubstitution\nfrom launch_ros.actions import Node, SetRemap\nfrom launch_ros.substitutions import FindPackageShare\n\ndef generate_launch_description():\n    use_sim = LaunchConfiguration('use_sim', default='false')\n    camera_config = LaunchConfiguration('camera_config', default='default')\n\n    # Camera driver\n    camera_node = Node(\n        package='usb_cam',\n        executable='usb_cam_node_exe',\n        name='camera_front',\n        parameters=[PathJoinSubstitution([\n            FindPackageShare('my_robot_bringup'), 'config', 'camera_front.yaml'\n        ])],\n        remappings=[('/image_raw', '/camera/front/image_raw')],\n    )\n\n    # LiDAR driver\n    lidar_node = Node(\n        package='sllidar_ros2',\n        executable='sllidar_node',\n        name='lidar',\n        parameters=[{\n            'serial_port': '/dev/robot/lidar',\n            'serial_baudrate': 460800,\n            'frame_id': 'lidar_link',\n            'angle_compensate': True,\n        }],\n    )\n\n    # IMU driver\n    imu_node = Node(\n        package='imu_driver',\n        executable='imu_node',\n        name='imu',\n        parameters=[{\n            'port': '/dev/robot/imu',\n            'frame_id': 'imu_link',\n            'publish_rate': 100.0,\n        }],\n    )\n\n    # Motor controller driver\n    motor_node = Node(\n        package='motor_driver',\n        executable='motor_controller_node',\n        name='motor_controller',\n        parameters=[PathJoinSubstitution([\n            FindPackageShare('my_robot_bringup'), 'config', 'motors.yaml'\n        ])],\n    )\n\n    return LaunchDescription([\n        DeclareLaunchArgument('use_sim', default_value='false'),\n        DeclareLaunchArgument('camera_config', default_value='default'),\n        camera_node,\n        lidar_node,\n        imu_node,\n        motor_node,\n    ])\n```\n\n### Perception Layer Launch\n\n```python\n# my_robot_bringup/launch/perception.launch.py\nfrom launch import LaunchDescription\nfrom launch.actions import DeclareLaunchArgument, GroupAction\nfrom launch.conditions import IfCondition\nfrom launch.substitutions import LaunchConfiguration, PathJoinSubstitution\nfrom launch_ros.actions import Node, ComposableNodeContainer, LoadComposableNode\nfrom launch_ros.descriptions import ComposableNode\nfrom launch_ros.substitutions import FindPackageShare\n\ndef generate_launch_description():\n    enable_slam = LaunchConfiguration('enable_slam', default='true')\n    enable_detection = LaunchConfiguration('enable_detection', default='true')\n\n    # Use a composable node container for zero-copy perception pipeline\n    perception_container = ComposableNodeContainer(\n        name='perception_container',\n        namespace='',\n        package='rclcpp_components',\n        executable='component_container_mt',\n        composable_node_descriptions=[\n            ComposableNode(\n                package='image_proc',\n                plugin='image_proc::RectifyNode',\n                name='rectify',\n                remappings=[('image', '/camera/front/image_raw')],\n            ),\n            ComposableNode(\n                package='my_detection',\n                plugin='my_detection::DetectorNode',\n                name='detector',\n                parameters=[PathJoinSubstitution([\n                    FindPackageShare('my_robot_bringup'), 'config', 'detector.yaml'\n                ])],\n            ),\n        ],\n        condition=IfCondition(enable_detection),\n    )\n\n    # SLAM node\n    slam_node = Node(\n        package='slam_toolbox',\n        executable='async_slam_toolbox_node',\n        name='slam',\n        parameters=[PathJoinSubstitution([\n            FindPackageShare('my_robot_bringup'), 'config', 'slam.yaml'\n        ])],\n        condition=IfCondition(enable_slam),\n    )\n\n    return LaunchDescription([\n        DeclareLaunchArgument('enable_slam', default_value='true'),\n        DeclareLaunchArgument('enable_detection', default_value='true'),\n        perception_container,\n        slam_node,\n    ])\n```\n\n### Application Layer Launch\n\n```python\n# my_robot_bringup/launch/application.launch.py\nfrom launch import LaunchDescription\nfrom launch.actions import DeclareLaunchArgument, IncludeLaunchDescription\nfrom launch.launch_description_sources import PythonLaunchDescriptionSource\nfrom launch.substitutions import LaunchConfiguration, PathJoinSubstitution\nfrom launch_ros.actions import Node\nfrom launch_ros.substitutions import FindPackageShare\n\ndef generate_launch_description():\n    nav_params = LaunchConfiguration('nav_params', default=PathJoinSubstitution([\n        FindPackageShare('my_robot_bringup'), 'config', 'nav2_params.yaml'\n    ]))\n\n    # Include Nav2 bringup\n    nav2_bringup = IncludeLaunchDescription(\n        PythonLaunchDescriptionSource(PathJoinSubstitution([\n            FindPackageShare('nav2_bringup'), 'launch', 'bringup_launch.py'\n        ])),\n        launch_arguments={\n            'params_file': nav_params,\n            'use_sim_time': LaunchConfiguration('use_sim', default='false'),\n        }.items(),\n    )\n\n    # Mission planner\n    mission_node = Node(\n        package='my_mission',\n        executable='mission_planner',\n        name='mission_planner',\n        parameters=[PathJoinSubstitution([\n            FindPackageShare('my_robot_bringup'), 'config', 'mission.yaml'\n        ])],\n    )\n\n    return LaunchDescription([\n        DeclareLaunchArgument('nav_params', default_value=''),\n        DeclareLaunchArgument('use_sim', default_value='false'),\n        nav2_bringup,\n        mission_node,\n    ])\n```\n\n### Top-Level Bringup Launch\n\nThis launch file composes all layers into a single entry point, with conditional arguments for simulation vs. real hardware and robot variant selection.\n\n```python\n# my_robot_bringup/launch/bringup.launch.py\nfrom launch import LaunchDescription\nfrom launch.actions import (\n    DeclareLaunchArgument, IncludeLaunchDescription,\n    GroupAction, LogInfo, TimerAction,\n)\nfrom launch.conditions import IfCondition, UnlessCondition\nfrom launch.launch_description_sources import PythonLaunchDescriptionSource\nfrom launch.substitutions import (\n    LaunchConfiguration, PathJoinSubstitution, PythonExpression,\n)\nfrom launch_ros.actions import PushRosNamespace, SetParameter\nfrom launch_ros.substitutions import FindPackageShare\n\ndef generate_launch_description():\n    pkg_share = FindPackageShare('my_robot_bringup')\n    use_sim = LaunchConfiguration('use_sim')\n    robot_variant = LaunchConfiguration('robot_variant')\n    enable_perception = LaunchConfiguration('enable_perception')\n    enable_navigation = LaunchConfiguration('enable_navigation')\n\n    # Hardware layer (skip in simulation)\n    hardware_launch = GroupAction(\n        actions=[\n            LogInfo(msg='Starting hardware layer...'),\n            IncludeLaunchDescription(\n                PythonLaunchDescriptionSource(\n                    PathJoinSubstitution([pkg_share, 'launch', 'hardware.launch.py'])\n                ),\n            ),\n        ],\n        condition=UnlessCondition(use_sim),\n    )\n\n    # Driver layer\n    drivers_launch = GroupAction(\n        actions=[\n            LogInfo(msg='Starting driver layer...'),\n            IncludeLaunchDescription(\n                PythonLaunchDescriptionSource(\n                    PathJoinSubstitution([pkg_share, 'launch', 'drivers.launch.py'])\n                ),\n                launch_arguments={'use_sim': use_sim}.items(),\n            ),\n        ],\n    )\n\n    # Perception layer (conditional)\n    perception_launch = GroupAction(\n        actions=[\n            LogInfo(msg='Starting perception layer...'),\n            IncludeLaunchDescription(\n                PythonLaunchDescriptionSource(\n                    PathJoinSubstitution([pkg_share, 'launch', 'perception.launch.py'])\n                ),\n            ),\n        ],\n        condition=IfCondition(enable_perception),\n    )\n\n    # Application layer (conditional)\n    application_launch = GroupAction(\n        actions=[\n            LogInfo(msg='Starting application layer...'),\n            IncludeLaunchDescription(\n                PythonLaunchDescriptionSource(\n                    PathJoinSubstitution([pkg_share, 'launch', 'application.launch.py'])\n                ),\n                launch_arguments={'use_sim': use_sim}.items(),\n            ),\n        ],\n        condition=IfCondition(enable_navigation),\n    )\n\n    return LaunchDescription([\n        DeclareLaunchArgument('use_sim', default_value='false',\n            description='Use simulation instead of real hardware'),\n        DeclareLaunchArgument('robot_variant', default_value='standard',\n            description='Robot variant: standard, heavy_payload, outdoor'),\n        DeclareLaunchArgument('enable_perception', default_value='true',\n            description='Enable perception stack'),\n        DeclareLaunchArgument('enable_navigation', default_value='true',\n            description='Enable navigation and application stack'),\n\n        # Set use_sim_time globally\n        SetParameter(name='use_sim_time', value=use_sim),\n\n        LogInfo(msg=['Bringing up robot variant: ', robot_variant]),\n\n        hardware_launch,\n        drivers_launch,\n        perception_launch,\n        application_launch,\n    ])\n```\n\n### Conditional Loading (Sim vs Real, Robot Variants)\n\nUse `IfCondition`, `UnlessCondition`, and `PythonExpression` to swap configurations based on runtime arguments.\n\n```python\nfrom launch.conditions import IfCondition, UnlessCondition\nfrom launch.substitutions import PythonExpression, LaunchConfiguration\n\nrobot_variant = LaunchConfiguration('robot_variant')\n\n# Load a variant-specific config file\nvariant_config = PathJoinSubstitution([\n    FindPackageShare('my_robot_bringup'), 'config', 'variants',\n    PythonExpression([\"'\", robot_variant, \"' + '.yaml'\"]),\n])\n\n# Conditional node: only load the arm driver for heavy_payload variant\narm_driver = Node(\n    package='arm_driver',\n    executable='arm_controller_node',\n    name='arm_controller',\n    condition=IfCondition(\n        PythonExpression([\"'\", robot_variant, \"' == 'heavy_payload'\"])\n    ),\n)\n```\n\n## Ordered Startup with Health Checks\n\n### Startup Dependency Graph\n\nNodes must start in a specific order to avoid subscribing to topics that do not yet exist or calling services before they are available.\n\n```\n                    ┌──────────────┐\n                    │  motors.srv  │\n                    └──────┬───────┘\n                           │\n              ┌────────────┼────────────┐\n              ▼            ▼            ▼\n        ┌──────────┐ ┌──────────┐ ┌──────────┐\n        │ camera   │ │  lidar   │ │   imu    │\n        └────┬─────┘ └────┬─────┘ └────┬─────┘\n             │            │            │\n             ▼            ▼            ▼\n        ┌──────────────────────────────────┐\n        │        perception / SLAM         │\n        └──────────────┬───────────────────┘\n                       │\n                       ▼\n        ┌──────────────────────────────────┐\n        │      navigation / planning       │\n        └──────────────────────────────────┘\n```\n\n### Health Check Scripts\n\nUse health check scripts in `ExecStartPre` to block service startup until dependencies are ready.\n\n```bash\n#!/bin/bash\n# /usr/local/bin/robot-device-check.sh\n# Verifies that all required hardware devices are present before starting drivers.\n# Exit code 0 = all devices found, non-zero = missing device.\n\nset -euo pipefail\n\nREQUIRED_DEVICES=(\n    \"/dev/robot/camera_front\"\n    \"/dev/robot/lidar\"\n    \"/dev/robot/imu\"\n    \"/dev/robot/motor_controller\"\n)\n\nTIMEOUT=30\nPOLL_INTERVAL=1\nelapsed=0\n\nfor device in \"${REQUIRED_DEVICES[@]}\"; do\n    elapsed=0\n    while [ ! -e \"$device\" ]; do\n        if [ \"$elapsed\" -ge \"$TIMEOUT\" ]; then\n            echo \"ERROR: Device $device not found after ${TIMEOUT}s\" >&2\n            exit 1\n        fi\n        echo \"Waiting for $device... (${elapsed}s/${TIMEOUT}s)\"\n        sleep \"$POLL_INTERVAL\"\n        elapsed=$((elapsed + POLL_INTERVAL))\n    done\n    echo \"Found device: $device\"\ndone\n\necho \"All required devices are present.\"\nexit 0\n```\n\n```bash\n#!/bin/bash\n# /usr/local/bin/wait-for-ros2-nodes.sh\n# Blocks until specified ROS2 nodes are active.\n# Usage: wait-for-ros2-nodes.sh node_name1 node_name2 ...\n\nset -euo pipefail\n\nsource /opt/ros/${ROS_DISTRO}/setup.bash\nsource /home/robot/ros2_ws/install/setup.bash\n\nTIMEOUT=60\nPOLL_INTERVAL=2\n\nfor node_name in \"$@\"; do\n    elapsed=0\n    while ! ros2 node list 2>/dev/null | grep -q \"$node_name\"; do\n        if [ \"$elapsed\" -ge \"$TIMEOUT\" ]; then\n            echo \"ERROR: Node $node_name not found after ${TIMEOUT}s\" >&2\n            exit 1\n        fi\n        echo \"Waiting for node $node_name... (${elapsed}s/${TIMEOUT}s)\"\n        sleep \"$POLL_INTERVAL\"\n        elapsed=$((elapsed + POLL_INTERVAL))\n    done\n    echo \"Node active: $node_name\"\ndone\n\necho \"All required nodes are active.\"\nexit 0\n```\n\n### Wait-for-Topic Pattern\n\nA reusable Python utility to block until a topic is being published, useful for ordered startup in launch files.\n\n```python\n#!/usr/bin/env python3\n# wait_for_topic.py\n# Usage: python3 wait_for_topic.py /scan sensor_msgs/msg/LaserScan --timeout 30\n\nimport argparse\nimport sys\nimport time\nimport importlib\n\nimport rclpy\nfrom rclpy.node import Node\nfrom rclpy.qos import qos_profile_sensor_data\n\n\nclass TopicWaiter(Node):\n    def __init__(self, topic_name, msg_type_str, timeout):\n        super().__init__('topic_waiter')\n        self.received = False\n        self.timeout = timeout\n        self.start_time = time.time()\n\n        # Dynamically import the message type\n        module_name, class_name = msg_type_str.rsplit('/', 1)\n        module_name = module_name.replace('/', '.')\n        module = importlib.import_module(module_name)\n        msg_type = getattr(module, class_name)\n\n        self.sub = self.create_subscription(\n            msg_type, topic_name, self._callback, qos_profile_sensor_data)\n        self.timer = self.create_timer(1.0, self._check_timeout)\n        self.get_logger().info(f'Waiting for topic {topic_name}...')\n\n    def _callback(self, msg):\n        self.get_logger().info('Topic is active, message received.')\n        self.received = True\n\n    def _check_timeout(self):\n        if self.received:\n            raise SystemExit(0)\n        elapsed = time.time() - self.start_time\n        if elapsed > self.timeout:\n            self.get_logger().error(f'Timeout after {self.timeout}s')\n            raise SystemExit(1)\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument('topic', help='Topic name to wait for')\n    parser.add_argument('msg_type', help='Message type (e.g., sensor_msgs/msg/LaserScan)')\n    parser.add_argument('--timeout', type=float, default=30.0)\n    args = parser.parse_args()\n\n    rclpy.init()\n    node = TopicWaiter(args.topic, args.msg_type, args.timeout)\n    rclpy.spin(node)\n\n\nif __name__ == '__main__':\n    main()\n```\n\n### Lifecycle Node Orchestration for Ordered Startup\n\nUse lifecycle (managed) nodes to enforce startup ordering. A lifecycle manager configures and activates nodes in sequence, ensuring each node completes its configuration before the next one starts.\n\n```python\n# lifecycle_manager.launch.py\nfrom launch import LaunchDescription\nfrom launch_ros.actions import Node\n\ndef generate_launch_description():\n    # Lifecycle manager controls the startup/shutdown order\n    lifecycle_manager = Node(\n        package='nav2_lifecycle_manager',\n        executable='lifecycle_manager',\n        name='lifecycle_manager',\n        output='screen',\n        parameters=[{\n            # Nodes are transitioned in order: configure, then activate\n            'node_names': [\n                'motor_controller',\n                'camera_driver',\n                'lidar_driver',\n                'slam',\n                'navigation',\n            ],\n            'autostart': True,\n            # Timeout for each node transition\n            'bond_timeout': 10.0,\n            # Check period for node bonds\n            'bond_respawn_max_duration': 2.0,\n        }],\n    )\n\n    return LaunchDescription([\n        lifecycle_manager,\n    ])\n```\n\n## udev Rules for Deterministic Device Naming\n\n### Writing udev Rules for Cameras\n\nUSB cameras can enumerate in any order on boot, causing `/dev/video0` to be unpredictable. Use udev rules to assign stable symlinks based on device attributes.\n\n```bash\n# /etc/udev/rules.d/99-robot-cameras.rules\n# Assign stable device names based on USB port path (physical location).\n# Find attributes with: udevadm info --name=/dev/video0 --attribute-walk\n\n# Front camera: USB hub port 1, interface 0 (video capture)\nSUBSYSTEM==\"video4linux\", ATTRS{idVendor}==\"1234\", ATTRS{idProduct}==\"5678\", \\\n  KERNELS==\"1-1.2:1.0\", ATTR{index}==\"0\", \\\n  SYMLINK+=\"robot/camera_front\", MODE=\"0666\", GROUP=\"video\"\n\n# Rear camera: USB hub port 2, interface 0 (video capture)\nSUBSYSTEM==\"video4linux\", ATTRS{idVendor}==\"1234\", ATTRS{idProduct}==\"5678\", \\\n  KERNELS==\"1-1.3:1.0\", ATTR{index}==\"0\", \\\n  SYMLINK+=\"robot/camera_rear\", MODE=\"0666\", GROUP=\"video\"\n\n# Depth camera (RealSense): by serial number\nSUBSYSTEM==\"video4linux\", ATTRS{idVendor}==\"8086\", ATTRS{idProduct}==\"0b3a\", \\\n  ATTRS{serial}==\"123456789\", ATTR{index}==\"0\", \\\n  SYMLINK+=\"robot/camera_depth\", MODE=\"0666\", GROUP=\"video\"\n```\n\n### Writing udev Rules for Serial Devices\n\nSerial devices (IMU, motor controller, GPS) also need stable names since `/dev/ttyUSB*` numbering is non-deterministic.\n\n```bash\n# /etc/udev/rules.d/99-robot-serial.rules\n# IMU on FTDI serial adapter (identified by serial number)\nSUBSYSTEM==\"tty\", ATTRS{idVendor}==\"0403\", ATTRS{idProduct}==\"6001\", \\\n  ATTRS{serial}==\"AB0CDEFG\", \\\n  SYMLINK+=\"robot/imu\", MODE=\"0666\", GROUP=\"dialout\"\n\n# Motor controller on USB port path\nSUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"7523\", \\\n  KERNELS==\"1-1.4:1.0\", \\\n  SYMLINK+=\"robot/motor_controller\", MODE=\"0666\", GROUP=\"dialout\"\n\n# GPS receiver\nSUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1546\", ATTRS{idProduct}==\"01a7\", \\\n  SYMLINK+=\"robot/gps\", MODE=\"0666\", GROUP=\"dialout\"\n\n# LiDAR (CP2102 adapter)\nSUBSYSTEM==\"tty\", ATTRS{idVendor}==\"10c4\", ATTRS{idProduct}==\"ea60\", \\\n  ATTRS{serial}==\"0001\", \\\n  SYMLINK+=\"robot/lidar\", MODE=\"0666\", GROUP=\"dialout\"\n```\n\n### Reloading and Testing\n\n```bash\n# Reload udev rules without rebooting\nsudo udevadm control --reload-rules\nsudo udevadm trigger\n\n# Test a rule against a specific device\nsudo udevadm test $(udevadm info --query=path --name=/dev/ttyUSB0)\n\n# View all attributes for a device (use to find idVendor, serial, etc.)\nudevadm info --name=/dev/ttyUSB0 --attribute-walk\n\n# Monitor udev events in real time (plug/unplug devices to see events)\nudevadm monitor --subsystem-match=tty --property\n```\n\n## Network Configuration for Multi-Machine ROS2\n\n### Static IP Configuration\n\nAssign a static IP to the robot's wired interface using netplan (Ubuntu 22.04+).\n\n```yaml\n# /etc/netplan/01-robot-network.yaml\nnetwork:\n  version: 2\n  ethernets:\n    eth0:\n      addresses:\n        - 10.0.0.10/24\n      routes:\n        - to: default\n          via: 10.0.0.1\n      nameservers:\n        addresses:\n          - 8.8.8.8\n          - 8.8.4.4\n  wifis:\n    wlan0:\n      dhcp4: true\n      access-points:\n        \"RobotNetwork\":\n          password: \"securepassword\"\n```\n\n```bash\n# Apply netplan configuration\nsudo netplan apply\n```\n\n### DDS Discovery Across Machines\n\nCycloneDDS requires explicit peer configuration for multi-machine setups since multicast may not work across network segments.\n\n```xml\n<!-- /etc/robot/cyclonedds.xml -->\n<CycloneDDS>\n  <Domain>\n    <General>\n      <Interfaces>\n        <NetworkInterface name=\"eth0\" priority=\"default\" multicast=\"false\"/>\n      </Interfaces>\n      <AllowMulticast>false</AllowMulticast>\n    </General>\n    <Discovery>\n      <ParticipantIndex>auto</ParticipantIndex>\n      <Peers>\n        <!-- Robot onboard computer -->\n        <Peer address=\"10.0.0.10\"/>\n        <!-- Base station / operator workstation -->\n        <Peer address=\"10.0.0.20\"/>\n        <!-- Second robot (if applicable) -->\n        <Peer address=\"10.0.0.11\"/>\n      </Peers>\n      <MaxAutoParticipantIndex>30</MaxAutoParticipantIndex>\n    </Discovery>\n    <Internal>\n      <SocketReceiveBufferSize min=\"10MB\"/>\n    </Internal>\n  </Domain>\n</CycloneDDS>\n```\n\n### Firewall Rules\n\nDDS uses a range of UDP ports for discovery and data exchange. Open these ports on both the robot and the base station.\n\n```bash\n#!/bin/bash\n# /usr/local/bin/robot-firewall-setup.sh\n# Open firewall ports for CycloneDDS discovery and data exchange.\n\n# DDS discovery (SPDP) uses UDP port 7400 + (250 * domain_id) + participant_id\n# For domain_id=42: base port = 7400 + 250*42 = 17900\nDOMAIN_ID=42\nBASE_PORT=$((7400 + 250 * DOMAIN_ID))\n\n# Allow discovery (SPDP) multicast/unicast\nsudo ufw allow proto udp from 10.0.0.0/24 to any port $BASE_PORT:$((BASE_PORT + 100))\n\n# Allow data exchange (SEDP) user traffic ports\nDATA_PORT=$((BASE_PORT + 1))\nsudo ufw allow proto udp from 10.0.0.0/24 to any port $DATA_PORT:$((DATA_PORT + 200))\n\n# Allow all traffic on the robot subnet (simpler alternative)\n# sudo ufw allow from 10.0.0.0/24\n\nsudo ufw reload\necho \"Firewall configured for ROS2 DDS on domain $DOMAIN_ID\"\n```\n\n### ROS_DOMAIN_ID and ROS_LOCALHOST_ONLY\n\n```bash\n# Isolate robots on the same network by domain ID (0-232)\nexport ROS_DOMAIN_ID=42\n\n# Lock DDS traffic to localhost only (useful for single-machine development)\nexport ROS_LOCALHOST_ONLY=1\n\n# For multi-machine setups, ensure ROS_LOCALHOST_ONLY is 0 on all machines\nexport ROS_LOCALHOST_ONLY=0\n\n# Verify DDS discovery across machines\nros2 daemon stop && ros2 daemon start\nros2 topic list  # Should see topics from both machines\n```\n\n## Watchdog and Heartbeat Monitoring\n\n### systemd Watchdog Integration\n\nWhen `WatchdogSec` is set in the service unit, the process must periodically notify systemd that it is alive. If the notification is missed, systemd restarts the service.\n\n```python\n#!/usr/bin/env python3\n# watchdog_node.py\n# A ROS2 node that integrates with systemd watchdog via sd_notify.\n\nimport os\nimport socket\nimport time\n\nimport rclpy\nfrom rclpy.node import Node\nfrom diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus\n\n\nclass WatchdogNode(Node):\n    \"\"\"Notifies systemd that the ROS2 process is alive.\"\"\"\n\n    def __init__(self):\n        super().__init__('watchdog_node')\n\n        # Read the watchdog interval from systemd environment\n        watchdog_usec = os.environ.get('WATCHDOG_USEC')\n        if watchdog_usec:\n            # Notify at half the watchdog interval for safety margin\n            interval_sec = int(watchdog_usec) / 1_000_000 / 2.0\n        else:\n            interval_sec = 10.0\n            self.get_logger().warn('WATCHDOG_USEC not set, using 10s interval')\n\n        # Connect to systemd notification socket\n        self.notify_socket = os.environ.get('NOTIFY_SOCKET')\n\n        # Signal that startup is complete\n        self._sd_notify('READY=1')\n        self.get_logger().info(\n            f'Watchdog node started, notify interval: {interval_sec:.1f}s')\n\n        # Periodically send watchdog keepalive\n        self.create_timer(interval_sec, self._watchdog_tick)\n\n        # Subscribe to system diagnostics to detect failures\n        self.diag_sub = self.create_subscription(\n            DiagnosticArray, '/diagnostics', self._diag_callback, 10)\n        self.system_healthy = True\n\n    def _watchdog_tick(self):\n        \"\"\"Send watchdog keepalive to systemd if system is healthy.\"\"\"\n        if self.system_healthy:\n            self._sd_notify('WATCHDOG=1')\n        else:\n            self.get_logger().error(\n                'System unhealthy, withholding watchdog notification')\n\n    def _diag_callback(self, msg):\n        \"\"\"Monitor diagnostics for critical errors.\"\"\"\n        for status in msg.status:\n            if status.level == DiagnosticStatus.ERROR:\n                self.get_logger().error(f'Critical error: {status.name}: {status.message}')\n                self.system_healthy = False\n                return\n        self.system_healthy = True\n\n    def _sd_notify(self, state):\n        \"\"\"Send notification to systemd.\"\"\"\n        if not self.notify_socket:\n            return\n        addr = self.notify_socket\n        if addr[0] == '@':\n            addr = '\\0' + addr[1:]\n        sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n        try:\n            sock.connect(addr)\n            sock.sendall(state.encode())\n        finally:\n            sock.close()\n\n\ndef main():\n    rclpy.init()\n    node = WatchdogNode()\n    rclpy.spin(node)\n    node.destroy_node()\n    rclpy.shutdown()\n\n\nif __name__ == '__main__':\n    main()\n```\n\n### ROS2-Level Heartbeat Monitor Node\n\nA monitor node that subscribes to heartbeat topics from critical subsystems and publishes overall system health. If a heartbeat is missed, it triggers a safe stop.\n\n```python\n#!/usr/bin/env python3\n# heartbeat_monitor.py\n# Monitors heartbeats from critical nodes and triggers safe stop if any go silent.\n\nimport time\n\nimport rclpy\nfrom rclpy.node import Node\nfrom rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy\nfrom std_msgs.msg import Bool, String\nfrom geometry_msgs.msg import Twist\nfrom diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue\n\n\nclass HeartbeatMonitor(Node):\n    def __init__(self):\n        super().__init__('heartbeat_monitor')\n\n        # Declare parameters for monitored nodes and timeout\n        self.declare_parameter('monitored_nodes', [\n            'motor_controller', 'camera_driver', 'lidar_driver', 'slam'\n        ])\n        self.declare_parameter('heartbeat_timeout_sec', 5.0)\n        self.declare_parameter('check_period_sec', 1.0)\n\n        self.monitored_nodes = self.get_parameter('monitored_nodes').value\n        self.timeout = self.get_parameter('heartbeat_timeout_sec').value\n        check_period = self.get_parameter('check_period_sec').value\n\n        # Track last heartbeat time for each monitored node\n        self.last_heartbeat = {name: time.time() for name in self.monitored_nodes}\n\n        # Subscribe to each node's heartbeat topic\n        reliable_qos = QoSProfile(\n            reliability=ReliabilityPolicy.RELIABLE,\n            durability=DurabilityPolicy.VOLATILE,\n            depth=1,\n        )\n        for node_name in self.monitored_nodes:\n            self.create_subscription(\n                Bool, f'/{node_name}/heartbeat',\n                lambda msg, n=node_name: self._heartbeat_callback(n, msg),\n                reliable_qos,\n            )\n\n        # Publishers\n        self.health_pub = self.create_publisher(\n            DiagnosticArray, '/system_health', 10)\n        self.estop_pub = self.create_publisher(\n            Bool, '/emergency_stop', reliable_qos)\n        self.cmd_vel_pub = self.create_publisher(\n            Twist, '/cmd_vel', 10)\n\n        # Periodic health check\n        self.create_timer(check_period, self._check_health)\n        self.get_logger().info(\n            f'Monitoring heartbeats for: {self.monitored_nodes}')\n\n    def _heartbeat_callback(self, node_name, msg):\n        \"\"\"Record heartbeat reception time.\"\"\"\n        self.last_heartbeat[node_name] = time.time()\n\n    def _check_health(self):\n        \"\"\"Check all heartbeats and publish diagnostics.\"\"\"\n        now = time.time()\n        diag_array = DiagnosticArray()\n        diag_array.header.stamp = self.get_clock().now().to_msg()\n        all_healthy = True\n\n        for node_name in self.monitored_nodes:\n            elapsed = now - self.last_heartbeat[node_name]\n            status = DiagnosticStatus()\n            status.name = f'heartbeat/{node_name}'\n\n            if elapsed < self.timeout:\n                status.level = DiagnosticStatus.OK\n                status.message = f'Alive ({elapsed:.1f}s ago)'\n            else:\n                status.level = DiagnosticStatus.ERROR\n                status.message = f'TIMEOUT ({elapsed:.1f}s since last heartbeat)'\n                all_healthy = False\n                self.get_logger().error(\n                    f'Heartbeat timeout for {node_name}: {elapsed:.1f}s')\n\n            status.values = [\n                KeyValue(key='elapsed_sec', value=f'{elapsed:.2f}'),\n                KeyValue(key='timeout_sec', value=f'{self.timeout:.2f}'),\n            ]\n            diag_array.status.append(status)\n\n        self.health_pub.publish(diag_array)\n\n        if not all_healthy:\n            self._trigger_safe_stop()\n\n    def _trigger_safe_stop(self):\n        \"\"\"Send zero velocity and emergency stop signal.\"\"\"\n        self.get_logger().warn('Triggering safe stop due to heartbeat failure')\n        # Publish zero velocity\n        self.cmd_vel_pub.publish(Twist())\n        # Publish emergency stop\n        estop_msg = Bool()\n        estop_msg.data = True\n        self.estop_pub.publish(estop_msg)\n\n\ndef main():\n    rclpy.init()\n    node = HeartbeatMonitor()\n    rclpy.spin(node)\n    node.destroy_node()\n    rclpy.shutdown()\n\n\nif __name__ == '__main__':\n    main()\n```\n\n### Hardware Watchdog Integration\n\nMany robot onboard computers have a hardware watchdog timer (e.g., Intel TCO, iTCO_wdt). If the software fails to pet the watchdog, the hardware performs a hard reboot.\n\n```bash\n# Enable the hardware watchdog in systemd\n# /etc/systemd/system.conf\n# RuntimeWatchdogSec=30\n# RebootWatchdogSec=10min\n# ShutdownWatchdogSec=10min\n\n# Or configure per-service in the unit file:\n# WatchdogSec=30 triggers systemd to restart the service\n# The hardware watchdog (configured via RuntimeWatchdogSec) reboots\n# the entire machine if systemd itself becomes unresponsive.\n\n# Verify hardware watchdog is active\nsudo cat /sys/class/watchdog/watchdog0/state\n# Should output: active\n\n# Check watchdog timeout\nsudo cat /sys/class/watchdog/watchdog0/timeout\n```\n\n## Logging and Log Rotation\n\n### ROS2 Log Configuration\n\n```bash\n# Set log level via environment\nexport RCUTILS_LOGGING_USE_STDOUT=0          # Log to stderr (captured by journald)\nexport RCUTILS_COLORIZED_OUTPUT=0            # Disable color codes in log files\nexport RCUTILS_CONSOLE_OUTPUT_FORMAT=\"[{severity}] [{time}] [{name}]: {message}\"\n\n# Set log level at runtime\nros2 run my_pkg my_node --ros-args --log-level debug\nros2 run my_pkg my_node --ros-args --log-level my_node:=debug\n\n# Set log level via parameter (Humble+)\nros2 param set /my_node use_sim_time false\nros2 service call /my_node/set_logger_level rcl_interfaces/srv/SetLoggerLevel \\\n  \"{logger_name: 'my_node', level: 10}\"\n```\n\n### journald Configuration for ROS2 Services\n\n```ini\n# /etc/systemd/journald.conf.d/robot.conf\n[Journal]\n# Persist logs across reboots\nStorage=persistent\n\n# Limit total journal size to 1 GB\nSystemMaxUse=1G\n\n# Limit per-file size to 100 MB\nSystemMaxFileSize=100M\n\n# Keep logs for 30 days\nMaxRetentionSec=30day\n\n# Rate limit: allow bursts during startup\nRateLimitIntervalSec=10s\nRateLimitBurst=10000\n\n# Forward to syslog for remote logging\nForwardToSyslog=yes\n```\n\n```bash\n# View logs for a specific robot service\njournalctl -u robot-drivers.service -f\n\n# View logs since last boot\njournalctl -u robot-bringup.service -b\n\n# View logs with priority filtering (error and above)\njournalctl -u robot-bringup.service -p err\n\n# Export logs for analysis\njournalctl -u robot-bringup.service --since \"2024-01-01\" --output=json > logs.json\n```\n\n### logrotate for ROS2 Log Files\n\nROS2 writes log files to `~/.ros/log/` by default, or to `$ROS_LOG_DIR`. These grow unbounded without rotation.\n\n```ini\n# /etc/logrotate.d/ros2\n/var/log/ros2/*.log {\n    daily\n    rotate 7\n    compress\n    delaycompress\n    missingok\n    notifempty\n    create 0644 robot robot\n    maxsize 100M\n    dateext\n    dateformat -%Y%m%d\n    postrotate\n        # Notify ROS2 nodes to reopen log files (if using file logging)\n        systemctl kill --signal=HUP robot-bringup.service 2>/dev/null || true\n    endscript\n}\n\n/home/robot/.ros/log/**/*.log {\n    daily\n    rotate 3\n    compress\n    missingok\n    notifempty\n    maxsize 50M\n}\n```\n\n### Structured Logging for Production\n\n```python\nimport json\nimport logging\nfrom rclpy.node import Node\n\n\nclass StructuredLogger:\n    \"\"\"Wraps ROS2 logger with structured JSON output for production monitoring.\"\"\"\n\n    def __init__(self, node: Node):\n        self.node = node\n        self.logger = node.get_logger()\n\n    def log_event(self, event_type: str, level: str = 'info', **kwargs):\n        \"\"\"Log a structured event with key-value metadata.\"\"\"\n        entry = {\n            'event': event_type,\n            'node': self.node.get_name(),\n            'namespace': self.node.get_namespace(),\n            'stamp': self.node.get_clock().now().nanoseconds,\n            **kwargs,\n        }\n        message = json.dumps(entry)\n        getattr(self.logger, level)(message)\n\n\n# Usage in a node:\n# self.slog = StructuredLogger(self)\n# self.slog.log_event('detection', count=5, latency_ms=12.3)\n# self.slog.log_event('motor_fault', level='error', motor_id=2, code=0x0A)\n```\n\n## Graceful Shutdown Sequences\n\n### Signal Handling in ROS2 Nodes\n\nROS2 nodes should handle `SIGINT` and `SIGTERM` to bring actuators to a safe state before exiting.\n\n```python\n#!/usr/bin/env python3\n# safe_shutdown_node.py\n# Demonstrates graceful shutdown with safe state transitions.\n\nimport signal\nimport sys\n\nimport rclpy\nfrom rclpy.node import Node\nfrom geometry_msgs.msg import Twist\nfrom std_msgs.msg import Bool\n\n\nclass SafeShutdownNode(Node):\n    def __init__(self):\n        super().__init__('safe_shutdown_node')\n\n        self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)\n        self.brake_pub = self.create_publisher(Bool, '/brakes/engage', 10)\n\n        # Register signal handlers for graceful shutdown\n        signal.signal(signal.SIGTERM, self._shutdown_handler)\n        signal.signal(signal.SIGINT, self._shutdown_handler)\n\n        self.get_logger().info('Node started with graceful shutdown handler')\n\n    def _shutdown_handler(self, signum, frame):\n        \"\"\"Handle shutdown signals by commanding safe state.\"\"\"\n        sig_name = signal.Signals(signum).name\n        self.get_logger().warn(f'Received {sig_name}, initiating safe shutdown...')\n\n        # Step 1: Command zero velocity immediately\n        zero_twist = Twist()  # All fields default to 0.0\n        for _ in range(5):\n            self.cmd_vel_pub.publish(zero_twist)\n\n        # Step 2: Engage brakes\n        brake_msg = Bool()\n        brake_msg.data = True\n        self.brake_pub.publish(brake_msg)\n\n        # Step 3: Wait briefly for commands to be received\n        self.get_logger().info('Safe state commanded, shutting down...')\n\n        # Step 4: Clean exit\n        self.destroy_node()\n        rclpy.shutdown()\n        sys.exit(0)\n\n\ndef main():\n    rclpy.init()\n    node = SafeShutdownNode()\n    rclpy.spin(node)\n    node.destroy_node()\n    rclpy.shutdown()\n\n\nif __name__ == '__main__':\n    main()\n```\n\n### Ordered Shutdown via systemd Dependencies\n\nThe `PartOf` and `Before` directives ensure that application-level services are stopped before drivers, preventing the situation where a navigation node sends velocity commands after the motor driver has exited.\n\n```ini\n# /etc/systemd/system/robot-application.service\n[Unit]\n# ...\nPartOf=robot-perception.service\nBefore=robot-perception.service\n\n# When robot-perception stops, robot-application is stopped FIRST\n# (Before= reverses the stop order relative to start order)\n\n[Service]\n# Use SIGINT for ROS2's signal handler, then SIGTERM, then SIGKILL\nKillSignal=SIGINT\nTimeoutStopSec=15\nFinalKillSignal=SIGTERM\nSendSIGKILL=yes\n```\n\n### Safe State on Shutdown\n\n```python\n# Use rclpy context shutdown callback for cleanup\nimport rclpy\nfrom rclpy.node import Node\n\n\nclass ActuatorNode(Node):\n    def __init__(self):\n        super().__init__('actuator_node')\n        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)\n        # Register a callback that runs during rclpy.shutdown()\n        context = self.context\n        context.on_shutdown(self._on_shutdown)\n\n    def _on_shutdown(self):\n        \"\"\"Called automatically during rclpy.shutdown().\"\"\"\n        self.get_logger().info('Shutdown callback: commanding zero velocity')\n        self.cmd_pub.publish(Twist())\n```\n\n## Remote Monitoring and Debugging\n\n### SSH Tunneling for ROS2 Topics\n\nForward DDS traffic over SSH when direct network connectivity is not available (e.g., robot is on a cellular connection).\n\n```bash\n#!/bin/bash\n# ssh-ros2-tunnel.sh\n# Creates an SSH tunnel for ROS2 DDS traffic between local machine and robot.\n# Usage: ./ssh-ros2-tunnel.sh robot@10.0.0.10\n\nset -euo pipefail\n\nROBOT_HOST=\"${1:?Usage: $0 robot@host}\"\nDOMAIN_ID=\"${ROS_DOMAIN_ID:-0}\"\nBASE_PORT=$((7400 + 250 * DOMAIN_ID))\n\necho \"Setting up SSH tunnel for ROS2 domain $DOMAIN_ID (ports $BASE_PORT-$((BASE_PORT + 200)))\"\n\n# Forward DDS discovery and data ports\nssh -N \\\n  -L ${BASE_PORT}:localhost:${BASE_PORT} \\\n  -L $((BASE_PORT + 1)):localhost:$((BASE_PORT + 1)) \\\n  -L $((BASE_PORT + 10)):localhost:$((BASE_PORT + 10)) \\\n  -L $((BASE_PORT + 11)):localhost:$((BASE_PORT + 11)) \\\n  \"$ROBOT_HOST\" &\n\nSSH_PID=$!\necho \"SSH tunnel PID: $SSH_PID\"\n\n# Set environment for local ROS2 to use localhost-only discovery\nexport ROS_LOCALHOST_ONLY=1\necho \"Run: export ROS_LOCALHOST_ONLY=1\"\necho \"Then use ros2 topic list, ros2 topic echo, etc.\"\necho \"Press Ctrl+C to close tunnel.\"\n\nwait $SSH_PID\n```\n\n### Remote journalctl and Service Management\n\n```bash\n# View live robot logs remotely\nssh robot@10.0.0.10 'journalctl -u robot-bringup.service -f'\n\n# Check service status\nssh robot@10.0.0.10 'systemctl status robot-drivers.service robot-perception.service'\n\n# Restart a single layer without rebooting\nssh robot@10.0.0.10 'sudo systemctl restart robot-perception.service'\n\n# View boot-time service ordering\nssh robot@10.0.0.10 'systemd-analyze blame | head -20'\n\n# Check for failed services\nssh robot@10.0.0.10 'systemctl --failed'\n\n# Stream structured logs as JSON\nssh robot@10.0.0.10 'journalctl -u robot-bringup.service -o json --follow'\n```\n\n### Deploying Updates via SSH\n\n```bash\n#!/bin/bash\n# deploy-to-robot.sh\n# Build locally, copy to robot, and restart services.\n# Usage: ./deploy-to-robot.sh robot@10.0.0.10\n\nset -euo pipefail\n\nROBOT_HOST=\"${1:?Usage: $0 robot@host}\"\nWORKSPACE=\"/home/robot/ros2_ws\"\n\necho \"=== Building workspace locally ===\"\ncolcon build --cmake-args -DCMAKE_BUILD_TYPE=Release --packages-select my_robot_bringup\n\necho \"=== Syncing to robot ===\"\nrsync -avz --delete \\\n  --exclude='build/' --exclude='log/' \\\n  src/ \"${ROBOT_HOST}:${WORKSPACE}/src/\"\n\necho \"=== Building on robot ===\"\nssh \"$ROBOT_HOST\" \"cd ${WORKSPACE} && \\\n  source /opt/ros/\\${ROS_DISTRO}/setup.bash && \\\n  colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release\"\n\necho \"=== Restarting robot services ===\"\nssh \"$ROBOT_HOST\" 'sudo systemctl restart robot-bringup.target'\n\necho \"=== Checking service status ===\"\nssh \"$ROBOT_HOST\" 'sleep 3 && systemctl status robot-bringup.target --no-pager'\n\necho \"Deploy complete.\"\n```\n\n## Robot Bringup Anti-Patterns\n\n### 1. Sourcing setup.bash in .bashrc for systemd\n\n**Problem:** systemd services do not load `~/.bashrc` or `~/.profile`. Environment variables set there are invisible to the service, causing ROS2 commands to fail with \"command not found\" or missing package errors.\n\n```bash\n# BAD: Relying on .bashrc for systemd services\n# ~/.bashrc\nsource /opt/ros/humble/setup.bash  # systemd will never see this\n\n# GOOD: Use EnvironmentFile in the service unit and source explicitly in ExecStart\n# /etc/robot/ros2.env\nROS_DISTRO=humble\nRMW_IMPLEMENTATION=rmw_cyclonedds_cpp\n\n# In the service unit:\n# EnvironmentFile=/etc/robot/ros2.env\n# ExecStart=/bin/bash -c 'source /opt/ros/${ROS_DISTRO}/setup.bash && ...'\n```\n\n### 2. No Startup Ordering\n\n**Problem:** 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, leading to intermittent startup failures.\n\n**Fix:** Use `After=` and `Requires=` in systemd units, or use a lifecycle manager to enforce ordered transitions:\n\n```ini\n# BAD: All services start in parallel with no ordering\n[Unit]\nDescription=Robot Navigation\n# No After= or Requires= directives\n\n# GOOD: Explicit dependency chain\n[Unit]\nDescription=Robot Navigation\nAfter=robot-perception.service\nRequires=robot-perception.service\n```\n\n### 3. Using Restart=always Without Rate Limiting\n\n**Problem:** A service that crashes on startup (e.g., missing config file, hardware disconnected) will restart in a tight loop, consuming CPU and flooding the journal.\n\n**Fix:** Use `StartLimitIntervalSec` and `StartLimitBurst` to cap restart attempts:\n\n```ini\n# BAD: Infinite restart loop\n[Service]\nRestart=always\nRestartSec=1\n\n# GOOD: Rate-limited restarts with failure notification\n[Service]\nRestart=on-failure\nRestartSec=5\nStartLimitIntervalSec=120\nStartLimitBurst=5\n```\n\n### 4. Relying on network.target Instead of network-online.target\n\n**Problem:** `network.target` is reached as soon as the network configuration starts, not when connectivity is actually established. DDS discovery fails because the network interface does not have an IP address yet.\n\n**Fix:** Use `network-online.target` and ensure `systemd-networkd-wait-online.service` or `NetworkManager-wait-online.service` is enabled:\n\n```ini\n# BAD: network.target does not guarantee connectivity\n[Unit]\nAfter=network.target\n\n# GOOD: Wait for actual network connectivity\n[Unit]\nAfter=network-online.target\nWants=network-online.target\n```\n\n### 5. No Log Rotation\n\n**Problem:** ROS2 log files in `~/.ros/log/` and journal entries grow without limit, eventually filling the disk on an embedded system with limited storage.\n\n**Fix:** Configure logrotate for ROS2 log files and set journald size limits:\n\n```bash\n# BAD: No log management\n# Logs in ~/.ros/log/ grow forever, disk fills up after weeks of operation\n\n# GOOD: logrotate config + journald limits\n# /etc/logrotate.d/ros2 (see Logging section above)\n# /etc/systemd/journald.conf: SystemMaxUse=1G\n```\n\n### 6. Hardcoded Device Paths (/dev/ttyUSB0)\n\n**Problem:** `/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.\n\n**Fix:** Use udev rules to create stable symlinks:\n\n```yaml\n# BAD: Hardcoded device path in ROS2 params\nserial_port: \"/dev/ttyUSB0\"  # Which device is this? It changes on reboot!\n\n# GOOD: Stable symlink via udev rule\nserial_port: \"/dev/robot/imu\"  # Always points to the correct device\n```\n\n### 7. Running the Entire Stack as Root\n\n**Problem:** Running ROS2 as root is a security risk and can cause permission issues with `rosbag2`, log files, and parameter persistence. A bug in a ROS2 node could damage the operating system.\n\n**Fix:** Create a dedicated `robot` user and grant only the necessary device permissions via udev `GROUP` and `MODE` rules:\n\n```bash\n# BAD: Running as root\n# ExecStart=/bin/bash -c 'source /opt/ros/humble/setup.bash && ros2 launch ...'\n# (runs as root because no User= is specified)\n\n# GOOD: Dedicated user with minimal privileges\n# Create robot user\nsudo useradd -r -m -s /bin/bash robot\nsudo usermod -aG dialout,video,plugdev robot\n\n# In the service unit:\n# User=robot\n# Group=robot\n\n# udev rules grant device access to the robot user's groups:\n# MODE=\"0666\", GROUP=\"dialout\"\n```\n\n### 8. No Graceful Shutdown Handler\n\n**Problem:** When systemd sends `SIGTERM` or `SIGINT` to stop a ROS2 node, the node exits immediately without commanding zero velocity or engaging brakes. The robot may coast or continue moving with the last commanded velocity.\n\n**Fix:** Register signal handlers or use rclpy's shutdown callback to command a safe state:\n\n```python\n# BAD: No shutdown handling, node just exits\ndef main():\n    rclpy.init()\n    node = MotorControlNode()\n    rclpy.spin(node)\n    # Robot is still moving with last commanded velocity!\n\n# GOOD: Shutdown handler commands safe state\ndef main():\n    rclpy.init()\n    node = MotorControlNode()\n    try:\n        rclpy.spin(node)\n    except KeyboardInterrupt:\n        pass\n    finally:\n        node.command_zero_velocity()\n        node.engage_brakes()\n        node.destroy_node()\n        rclpy.shutdown()\n```\n\n## Robot Bringup Checklist\n\n1. **udev rules written and tested** for all USB devices (cameras, LiDARs, serial adapters) with stable symlinks under `/dev/robot/`\n2. **systemd service units created** for each layer (drivers, perception, application) with correct `After=`/`Requires=` ordering\n3. **ROS2 environment file** (`/etc/robot/ros2.env`) configured with `ROS_DISTRO`, `RMW_IMPLEMENTATION`, `ROS_DOMAIN_ID`, and `CYCLONEDDS_URI`\n4. **CycloneDDS or FastDDS XML** configured with explicit peer list for multi-machine discovery\n5. **Launch files layered and composable** with conditional arguments for sim/real and robot variants\n6. **Health check scripts** written for `ExecStartPre` to verify device presence before starting drivers\n7. **Watchdog integration** configured: `WatchdogSec` in service units and `sd_notify(WATCHDOG=1)` in the ROS2 process\n8. **Heartbeat monitor node** deployed to detect node failures and trigger safe stop\n9. **Graceful shutdown handlers** registered in all actuator nodes (zero velocity, engage brakes on `SIGINT`/`SIGTERM`)\n10. **Log rotation configured** via logrotate for `$ROS_LOG_DIR` and journald `SystemMaxUse` limits\n11. **Restart policies rate-limited** with `StartLimitIntervalSec` and `StartLimitBurst` to prevent restart loops\n12. **Resource limits set** via `MemoryMax`, `CPUQuota` to prevent runaway nodes from starving the system\n13. **Network and firewall configured** with static IPs, DDS port rules, and `ROS_LOCALHOST_ONLY` set correctly\n14. **Full boot test performed** from power-off to autonomous operation, verifying service ordering and recovery from simulated failures","tags":["robot","bringup","robotics","agent","skills","arpitg1304","agent-skills","ai-coding-assistant","claude-skills"],"capabilities":["skill","source-arpitg1304","skill-robot-bringup","topic-agent-skills","topic-ai-coding-assistant","topic-claude-skills","topic-robotics"],"categories":["robotics-agent-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/arpitg1304/robotics-agent-skills/robot-bringup","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add arpitg1304/robotics-agent-skills","source_repo":"https://github.com/arpitg1304/robotics-agent-skills","install_from":"skills.sh"}},"qualityScore":"0.544","qualityRationale":"deterministic score 0.54 from registry signals: · indexed on github topic:agent-skills · 189 github stars · SKILL.md body (56,730 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-02T18:54:20.708Z","embedding":null,"createdAt":"2026-04-18T22:05:33.419Z","updatedAt":"2026-05-02T18:54:20.708Z","lastSeenAt":"2026-05-02T18:54:20.708Z","tsv":"'-0':4815 '-01':4218,4219 '-1.2':2645 '-1.3':2676 '-1.4':2780 '-20':4984 '-232':3143 '-5':951 '/.bashrc':5144,5177 '/.profile':5146 '/.ros/log':4233,5452,5489 '/24':2928,3060,3088,3111 '/bin/bash':375,606,670,733,2037,2137,3007,4781,5013,5213,5653,5681 '/bin/kill':402 '/brakes/engage':4480 '/camera/front/image_raw':1230,1424 '/cmd_vel':3723,4473,4720 '/deploy-to-robot.sh':5024 '/dev/null':2179,4286 '/dev/robot':5838 '/dev/robot/camera_front':1095,2066 '/dev/robot/imu':1120,1273,2068,5582 '/dev/robot/lidar':1108,1247,2067 '/dev/robot/motor_controller':2069 '/dev/ttyusb':2730 '/dev/ttyusb0':2857,2873,5516,5518,5543,5565 '/dev/ttyusb1':5538 '/dev/video0':2587,2621 '/diagnostics':3388 '/emergency_stop':3714 '/etc/logrotate.d/ros2':4247,5504 '/etc/netplan/01-robot-network.yaml':2920 '/etc/robot/config':555 '/etc/robot/cyclonedds.xml':513 '/etc/robot/ros2.env':350,479,604,668,731,5197,5211,5859 '/etc/security/limits.d':949 '/etc/systemd/journald.conf':5509 '/etc/systemd/journald.conf.d/robot.conf':4123 '/etc/systemd/system':295 '/etc/systemd/system.conf':3957 '/etc/systemd/system/robot-application.service':710,4641 '/etc/systemd/system/robot-bringup.service':313 '/etc/systemd/system/robot-drivers.service':581 '/etc/systemd/system/robot-perception.service':646 '/etc/udev/rules.d/99-robot-cameras.rules':2603 '/etc/udev/rules.d/99-robot-serial.rules':2737 '/heartbeat':3690 '/home/robot':339 '/home/robot/.ros/log':4289 '/home/robot/ros2_ws':5038 '/home/robot/ros2_ws/install/setup.bash':383,614,678,741,2161 '/image_raw':1229 '/my-org/my-robot':323 '/my_node':4100 '/my_node/set_logger_level':4108 '/opt/ros':378,609,673,736,2156,5084,5216 '/opt/ros/humble/setup.bash':5179,5656 '/scan':2267 '/setup.bash':381,612,676,739,2159,5087,5219 '/src':5073 '/ssh-ros2-tunnel.sh':4797 '/sys/class/watchdog/watchdog0/state':4003 '/sys/class/watchdog/watchdog0/timeout':4012 '/system_health':3707 '/tmp':967 '/usr/bin/env':2261,3240,3538,4428 '/usr/local/bin/robot-device-check.sh':360,2038 '/usr/local/bin/robot-firewall-setup.sh':3008 '/usr/local/bin/wait-for-ros2-nodes.sh':2138 '/var/log/ros2':533,966,4248 '0':527,538,542,955,2052,2076,2084,2135,2173,2235,2389,2632,2649,2663,2680,2706,3142,3176,3184,3473,3475,4031,4042,4589,4807,5034 '0.0':4544 '000':3319,3320 '0001':2817 '01':551 '01a7':2797 '0403':2751 '0644':4258 '0666':2653,2684,2710,2761,2785,2801,2821,5710 '0b3a':2700 '0x0a':4402 '1':429,2074,2105,2202,2326,2407,2630,2644,2675,2779,3080,3165,3318,3353,3412,3477,3677,4136,4532,4805,4855,4859,4901,4908,5032,5131,5354,5820,5927 '1.0':2356,2646,2677,2781,3622 '10':754,3390,3708,3724,4116,4474,4481,4721,4863,4867,5961 '10.0':2551,3325 '10.0.0.0':3059,3087,3110 '10.0.0.1':2933 '10.0.0.10':2927,4799,4942,4952,4965,4978,4991,5001,5026 '100':3068,4146 '100.0':1280 '10000':4166 '100m':4149,4262 '10c4':2811 '10min':3961,3963 '10s':3334,4164 '11':4871,4875,5975 '12':5989 '12.3':4391 '120':818,821,5371 '1234':2639,2670 '123456789':2703 '13':6004 '14':6021 '15':4682 '1546':2794 '17900':3039 '1800m':915 '1a86':2774 '1f':3365,3810,3820,3838 '1g':4139,5511 '2':905,2103,2166,2178,2200,2661,2923,4285,4400,4553,5220,5839 '2.0':1130,2561,3321 '2.5':910 '20':635,699,762 '200':3096,4837 '2024':4217 '22.04':2918 '250':3025,3037,3046,4819 '2f':3848,3856 '2g':913 '3':920,4293,4565,5116,5304,5855 '30':406,434,629,693,756,2071,2271,2980,3959,3974,4153 '30.0':2437 '300':919,929 '30day':4156 '4':4582,5374,5872 '42':506,3033,3038,3042,3148 '460800':1250 '5':420,627,691,810,814,823,843,4388,4548,5369,5373,5443,5887 '5.0':3616 '50m':4298 '5678':2642,2673 '6':5512,5901 '60':444,2163 '6001':2754 '7':4252,5589,5915 '7400':3024,3036,3045,4818 '7523':2777 '8':5713,5932 '8.8.4.4':2937 '8.8.8.8':2936 '8086':2697 '9':5945 'ab0cdefg':2757 'access':958,2943,5702 'access-point':2942 'across':133,2957,2974,3188,4127 'action':1131,1719,1741,1767,1790 'activ':2145,2224,2233,2376,2473,2531,4000,4006 'actual':5396,5435 'actuat':166,4420,4713,5952 'actuatornod':4706 'adapt':2742,2806,5833 'add':796,897 'addr':3468,3472,3474,3476,3486 'address':2926,2935,5410 'advertis':5247 'ag':5685 'ago':3812 'alert':871,875 'aliv':3229,3281,3808 'allow':571,811,3049,3055,3069,3083,3097,3108,4159 'also':833,2725 'altern':3105 'alway':5307,5352,5583 'analysi':4212 'analyz':4981 'angl':1255 'anti':5129 'anti-pattern':5128 'appli':2949,2954 'applic':91,203,214,714,768,1492,1784,1787,1794,1862,1891,4617,4654,5849 'application-level':202,4616 'application.launch.py':748,1029,1802 'architectur':977,988 'arg':2438,2440,4071,4084,5047,5092 'argpars':2273 'argparse.argumentparser':2411 'args.msg':2445 'args.timeout':2447 'args.topic':2444 'argument':1066,1558,1629,1755,1804,1911,2413,2422,2432,5895 'arm':1953,1959,1963,1966,1970 'array':3771,3861 'assign':2595,2604,2905,5521 'async':1456 'attempt':816,5237,5344 'attr':2637,2640,2647,2668,2671,2678,2695,2698,2701,2704,2749,2752,2755,2772,2775,2792,2795,2809,2812,2815 'attribut':2601,2616,2623,2860,2875 'attribute-walk':2622,2874 'auto':2979 'automat':61,4739 'autonom':6031 'autostart':2542 'avail':2010,4772 'avoid':104,499,829,1995 'avz':5063 'b':4195 'bad':5170,5274,5346,5423,5483,5556,5648,5769 'base':15,136,1908,2598,2608,3004,3034,3043,3064,3066,3078,4816,4833,4835,4847,4850,4853,4857,4861,4865,4869,4873 'bash':367,478,2036,2136,2602,2736,2827,2948,3006,3132,3950,4020,4175,4780,4934,5012,5169,5482,5647 'bashrc':471,5135,5173 'basic':287 'baudrat':1249 'becom':3994,5537 'best':6 'bios/uefi':270 'blame':4982 'block':2029,2139,2246 'bond':2549,2556,2557 'bool':3571,3686,3713,3899,4455,4479,4558 'boot':47,68,175,268,276,2585,4191,4972,6023 'boot-tim':174,4971 'brake':4555,4556,4562,5740,5813,5957 'brake_msg.data':4559 'briefli':4567 'bring':9,165,1879,4419 'bringup':3,50,95,187,192,318,389,453,620,684,747,1225,1302,1440,1467,1541,1546,1548,1554,1591,1608,1614,1690,1941,5057,5127,5818 'bringup.launch.py':390,1002 'bringup/launch/application.launch.py':1498 'bringup/launch/bringup.launch.py':1642 'bringup/launch/drivers.launch.py':1155 'bringup/launch/hardware.launch.py':1041 'bringup/launch/perception.launch.py':1333 'bringup_launch.py':1556 'broken':789 'bug':5618 'build':5015,5040,5044,5049,5066,5075,5089,5094 'burst':4160 'c':376,397,607,671,734,4922,5214,5654 'call':425,2005,4107,4738,5239 'callback':2368,3424,3744,4696,4724,4746,5762 'cam':1211,1214 'camera':118,233,587,1015,1090,1098,1142,1197,1200,1204,1206,1218,1314,1319,2012,2536,2576,2578,2626,2657,2688,3606,5830 'camera.launch.py':1020 'camera_front.yaml':1227 'cap':5342 'captur':2634,2665,4035 'cat':4002,4011 'caus':2586,5156,5230,5607 'cd':5081 'cellular':4778 'cgroup':880 'chain':5295 'chang':5571 'check':102,249,354,1011,1083,1089,1097,1102,1110,1114,1122,1135,1141,1143,1145,1983,2020,2024,2382,2552,3619,3637,3641,3727,3730,3759,3762,4007,4947,4985,5109,5903 'checklist':5819 'class':2293,2323,2339,3271,3583,4312,4456,4705 'clean':413,4583 'cleanup':4698 'clearer':577 'clock':3775,4366 'close':4924 'cloud':227 'cmake':5046,5091 'cmake-arg':5045,5090 'cmd':1092,1105,1117 'coast':5744 'code':2051,4045,4401 'colcon':5043,5088 'color':540,4040,4044 'command':4513,4533,4569,4578,4633,4747,5158,5162,5735,5751,5764,5789,5794 'compens':1256 'complet':12,2480,3350,5125 'compon':1404,1406 'compos':84,1006,1386,1409,1619,5892 'composablenod':1361,1412,1425 'composablenodecontain':1356,1397 'composit':29,972 'compress':4253,4294 'comput':23,280,3925 'condit':106,1443,1470,1628,1732,1763,1780,1786,1810,1893,1948,1972,5232,5894 'config':260,553,1198,1201,1226,1303,1315,1441,1468,1542,1592,1933,1936,1942,5320,5501 'configur':39,57,123,508,529,546,777,793,1069,1907,2471,2482,2529,2896,2904,2951,2963,3117,3965,3984,4019,4118,5390,5471,5860,5877,5918,5964,6008 'connect':3336,4769,4779,5394,5428,5437 'consol':4051 'constrain':881 'consum':5330 'contain':1388,1396,1400,1407,1489 'context':4694,4729 'context.on':4731 'continu':5746 'control':238,1282,1292,1296,1967,1971,2504,2535,2723,2765,2835,3605,5542 'copi':1392,5017 'core':921,926 'correct':76,5587,5851,6020 'could':5623 'count':4387 'cp2102':2805 'cpp':492,5205 'cpu':917,5331 'cpuquota':928,5995 'crash':5315 'creat':4257,4783,5552,5629,5673,5843 'critic':356,939,1085,3430,3443,3520,3544 'cross':501 'cross-talk':500 'ctrl':396,4921 'cyclonedd':124,491,507,511,2959,3013,5204,5870,5873 'd':4267 'daemon':3191,3194 'daili':4250,4291 'damag':5624 'data':2292,2352,2993,3016,3070,3076,3092,3094,4842 'dateext':4263 'dateformat':4264 'day':4154 'dcmake':5048,5093 'dds':82,259,485,2955,2983,3018,3120,3150,3186,4762,4789,4839,5398,6012 'debug':173,4075,4090,4755 'declar':1065,3593 'declarelaunchargu':1163,1307,1313,1341,1476,1482,1506,1596,1601,1650,1816,1829,1842,1852 'dedic':346,466,5631,5668 'def':1061,1186,1366,1527,1681,2296,2367,2381,2408,2498,3282,3394,3422,3454,3491,3586,3742,3758,3867,3905,4324,4334,4459,4503,4590,4708,4734,5776,5797 'default':1075,1079,1081,1195,1202,1203,1310,1316,1318,1375,1382,1479,1485,1536,1569,1599,1604,1819,1832,1845,1855,2436,2931,4235,4542 'delay':827 'delaycompress':4254 'delet':5064 'demonstr':4431 'depend':108,208,556,1985,2033,4608,5294,5527 'deploy':159,5008,5124,5936 'deploy-to-robot.sh':5014 'depth':2687,3676 'descript':315,583,648,712,1064,1168,1189,1369,1411,1510,1530,1662,1684,1822,1835,1848,1858,2501,5284,5297 'detect':224,653,1025,1378,1381,1428,1431,1446,1484,3381,4386,5938 'detection.launch.py':1028 'detector':1434 'detector.yaml':1442 'detectornod':1432 'determinist':114,2569,2735 'develop':3160 'devic':115,122,182,244,357,792,1012,1086,1137,2044,2054,2060,2065,2078,2081,2087,2096,2097,2110,2125,2126,2131,2570,2600,2606,2718,2720,2848,2863,2884,5514,5526,5558,5567,5588,5639,5701,5829,5910 'dgram':3483 'dhcp4':2940 'diag':3423,3770,3860 'diag_array.header.stamp':3773 'diag_array.status.append':3857 'diagnost':3379,3428,3767 'diagnostic_msgs.msg':3267,3578 'diagnosticarray':3269,3387,3580,3706,3772 'diagnosticstatus':3270,3581,3795 'diagnosticstatus.error':3438,3815 'diagnosticstatus.ok':3805 'dialout':2763,2787,2803,2823,5686,5712 'dir':532,554,4240,5970 'direct':4613,4767,5291 'disabl':514,4043 'disconnect':5323 'discoveri':132,2956,2991,3014,3019,3050,3187,4840,4896,5399,5886 'disk':5462,5492 'distribut':481 'distro':380,483,611,675,738,2158,5086,5199,5218,5863 'document':320 'domain':257,493,504,3026,3031,3040,3047,3122,3123,3126,3140,3146,4810,4813,4820,4829,4830,5867 'done':2122,2127,2221,2227 'driver':89,231,234,236,586,641,940,1019,1149,1205,1232,1259,1265,1283,1289,1736,1738,1745,1887,1954,1960,1964,2049,2537,2539,3607,3609,4623,4637,5250,5847,5914 'drivers.launch.py':621,1014,1753 'drop':839 'drop-in':838 'due':3885 'durabilitypolici':3567 'durabilitypolicy.volatile':3675 'durabl':3674 'durat':2560 'dynam':836,2316 'e':1094,1107,1119,2086 'e.g':790,2428,3931,4773,5318 'ea60':2814 'echo':2094,2107,2123,2128,2190,2204,2222,2228,3115,4822,4880,4902,4909,4917,4919,5039,5058,5074,5097,5108,5123 'elaps':2075,2083,2090,2111,2118,2119,2172,2186,2210,2217,2218,2390,2395,3788,3802,3809,3819,3837,3843,3847 'els':3322,3413,3813 'embed':5465 'emerg':3876,3895 'enabl':1370,1373,1377,1380,1445,1472,1477,1483,1701,1704,1706,1709,1782,1812,1843,1849,1853,1859,3951,5421 'endscript':4288 'enforc':2465,5270 'engag':4554,5739,5956 'ensur':2477,3171,4614,5416 'enter':855 'entir':3989,5592 'entri':1625,4354,4372,5455 'enumer':183,245,2580,5529 'env':347 'environ':83,251,342,457,462,3295,4025,4887,5147,5857 'environmentfil':349,603,667,730,5187,5210 'environmentvari':1060,1076 'equival':398 'err':4208 'error':794,2095,2191,2399,3416,3431,3441,3444,3830,4201,4397,5168 'establish':5397 'estop':3897,3903 'estop_msg.data':3900 'etc':2869,4918 'eth0':2925 'ethernet':2924 'euo':2062,2153,4801,5028 'event':2879,2887,4336,4338,4348,4355,4356,4385,4393 'eventu':5459 'except':5805 'exchang':2994,3017,3071 'exclud':5065,5067 'exe':1216 'exec':384,615,679,742 'execstart':374,605,669,732,5196,5212,5652 'execstartpr':359,2027,5907 'execstop':401 'execut':1212,1239,1266,1290,1405,1455,1580,1965,2515 'executeprocess':1050,1091,1104,1116 'exist':358,2003 'exit':172,414,2050,2104,2134,2201,2234,4426,4584,4639,5732,5775 'explicit':568,2961,5194,5293,5879 'export':3144,3161,3180,4026,4038,4049,4209,4897,4904 'f':2361,2400,3357,3442,3687,3736,3797,3807,3817,3831,3846,3854,4186,4524,4946 'fail':856,862,3939,4987,4993,5160,5400 'failur':177,409,418,578,625,689,752,775,808,868,3382,3888,5255,5361,5367,5940,6040 'fals':1196,1312,1570,1606,1821,2310,2978,3449,3827,4104 'fast':847 'fastdd':126,5875 'fault':4395 'fi':2106,2203 'field':4541 'file':28,74,87,293,302,348,365,467,509,971,980,995,1560,1618,1934,2259,3972,4048,4143,4227,4231,4275,4278,5321,5450,5476,5613,5858,5889 'filesystem':957 'fill':5460,5493 'filter':228,4200 'final':867,3489,5808 'finalkillsign':441,4683 'find':2615,2866 'findpackageshar':1185,1222,1299,1365,1437,1464,1526,1538,1552,1588,1680,1687,1938 'firewal':2981,3010,3116,6007 'firmwar':248 'first':395,844,4657 'fix':5256,5336,5412,5470,5547,5628,5753 'float':2435 'flood':5333 'follow':193,5007 'forev':5491 'format':4053 'forward':4167,4761,4838 'forwardtosyslog':4173 'found':2055,2099,2124,2196,5164 'frame':1251,1274,4508 'front':1099,1219,2625 'ftdi':2740 'full':64,6022 'fundament':788 'fusion':230,655,1026 'gb':906,911,4137 'ge':2091,2187 'generat':1062,1187,1367,1528,1682,2499 'geometry_msgs.msg':3574,4449 'getattr':2337,4373 'github.com':322 'github.com/my-org/my-robot':321 'global':1868 'go':3552 'good':5185,5292,5355,5432,5499,5574,5667,5791 'gps':2724,2788 'grace':161,391,4403,4432,4486,4500,5715,5946 'grant':5635,5700 'graph':1986 'grep':2180 'group':336,2654,2685,2711,2762,2786,2802,2822,5643,5696,5708,5711 'groupact':1165,1342,1652,1718,1740,1766,1789 'grow':4242,5456,5490 'grub':271 'guarante':5427 'half':3306 'handl':4407,4414,4509,5772 'handler':163,4484,4502,4505,4674,5717,5756,5793,5948 'hard':907,3948 'hardcod':5513,5557 'hardwar':88,199,240,275,585,1035,1068,1134,1634,1711,1716,1723,1828,1885,2043,3919,3928,3945,3953,3982,3997,5322 'hardware.launch.py':1009,1731 'head':4983 'health':101,1982,2019,2023,3526,3726,3760,5902 'healthi':3392,3406,3409,3448,3452,3780,3826,3865 'heartbeat':141,3207,3508,3517,3529,3542,3591,3613,3633,3647,3654,3667,3738,3743,3750,3754,3764,3791,3798,3824,3832,3887,5933 'heartbeat_monitor.py':3540 'heartbeatmonitor':3584,3909 'heavi':1839,1956,1977 'help':2415,2425 'hit':852 'host':4804,4809,4877,5031,5036,5071,5080,5103,5114 'hri':220,718,1032 'hub':2628,2659 'humbl':484,4096,5200 'hup':4283 'id':258,505,1252,1275,3027,3029,3032,3041,3048,3124,3127,3141,3147,4399,4811,4814,4821,4831,5868 'identifi':2743 'idproduct':2641,2672,2699,2753,2776,2796,2813 'idvendor':2638,2669,2696,2750,2773,2793,2810,2867 'ifcondit':1055,1346,1444,1471,1658,1781,1811,1901,1916,1973 'imag':1414,1417,1423 'immedi':4536,5733 'implement':138,489,5202,5865 'import':1044,1048,1054,1058,1158,1162,1170,1174,1179,1184,1336,1340,1345,1349,1354,1360,1364,1501,1505,1512,1516,1521,1525,1645,1649,1657,1664,1668,1674,1679,1915,1920,2272,2274,2276,2278,2280,2284,2288,2317,2492,2496,3254,3256,3258,3260,3264,3268,3554,3556,3560,3564,3570,3575,3579,4304,4306,4310,4438,4440,4442,4446,4450,4454,4699,4703 'importlib':2279 'importlib.import':2331 'imu':239,589,1017,1115,1123,1146,1258,1260,1264,1267,1270,1276,1323,2014,2721,2738,5535 'includ':24,1544 'includelaunchdescript':1164,1507,1549,1651,1725,1747,1773,1796 'independ':572,993 'index':2648,2679,2705 'infinit':5347 'info':2360,2373,2619,2853,2871,3356,3735,4343,4496,4575,4744 'ini':312,580,645,709,795,896,4122,4246,4640,5273,5345,5422 'init':274,2297,2306,3283,3286,3587,3590,4325,4460,4463,4709,4712 'initi':200,4528 'instal':454,642,706,769 'instead':1825,5378 'int':403,3315 'integr':3211,3247,3921,5917 'intel':3932 'interfac':2631,2662,2914,5404 'interfaces/srv/setloggerlevel':4110 'intermitt':5253 'interv':432,2073,2117,2121,2165,2216,2220,3292,3309,3313,3323,3335,3362,3363,3373 'invis':5152 'ioschedulingclass':952 'ioschedulingprior':954 'ip':2903,2908,5409,6011 'isol':494,579,1001,3133 'issu':180,5609 'itco':3934 'item':1571,1760,1809 'journal':447,449,637,701,764,4124,4133,5335,5454 'journalctl':4183,4192,4204,4213,4930,4943,5002 'journald':4037,4117,5479,5502,5972 'json':4221,4305,4319,4998,5006 'json.dumps':4371 'keep':4150 'keepal':3370,3400 'kernel':272,2643,2674,2778 'key':3842,3850,4351 'key-valu':4350 'keyboardinterrupt':5806 'keyvalu':3582,3841,3849 'kill':908,4281 'killmod':437,630,694,757 'killsign':439,632,696,759,4679 'kwarg':4344,4369 'l':4846,4852,4860,4868 'lambda':3691 'last':3646,3823,4190,5750,5788 'latenc':4389 'launch':27,86,301,364,386,617,681,744,970,975,979,994,1037,1043,1063,1151,1157,1188,1329,1335,1368,1494,1500,1529,1555,1557,1615,1617,1644,1683,1717,1730,1739,1752,1754,1765,1778,1788,1801,1803,1886,1888,1890,1892,2258,2491,2500,5658,5888 'launch.actions':1047,1161,1339,1504,1648 'launch.conditions':1053,1344,1656,1914 'launch.launch':1167,1509,1661 'launch.substitutions':1057,1173,1348,1515,1667,1919 'launch_ros.actions':1178,1353,1520,1673,2495 'launch_ros.descriptions':1359 'launch_ros.substitutions':1183,1363,1524,1678 'launchconfigur':1059,1072,1175,1192,1199,1350,1372,1379,1517,1533,1566,1669,1693,1698,1703,1708,1922,1925 'launchdescript':1045,1140,1159,1306,1337,1475,1502,1595,1646,1815,2493,2563 'layer':85,195,207,215,222,232,241,575,715,974,976,982,990,1008,1036,1150,1328,1493,1621,1712,1724,1737,1746,1762,1772,1785,1795,4960,5846,5890 'lead':5251 'level':204,1005,1613,3507,4023,4060,4074,4087,4093,4115,4341,4375,4396,4618 'lidar':119,235,588,1016,1103,1111,1144,1231,1233,1243,1253,1321,2013,2538,2804,3608,5831 'lidar.launch.py':1021 'lifecycl':2454,2461,2469,2502,2508,2513,2516,2519,2564,5267 'lifecycle_manager.launch.py':2489 'limit':779,878,902,916,4131,4140,4158,5310,5358,5458,5468,5481,5503,5974,5980,5991 'link':1254,1277 'linux':267 'list':2177,3198,4914,5881 'live':4936 'load':340,475,1894,1928,1951,5143 'loadcomposablenod':1357 'local':4792,4889,5016,5042 'localhost':516,525,3130,3153,3163,3173,3182,4849,4856,4864,4872,4894,4899,4906,6017 'localhost-on':515,4893 'locat':2614 'lock':3149 'log':149,153,445,528,531,535,1126,1147,4013,4015,4018,4022,4028,4032,4047,4059,4073,4086,4092,4126,4151,4172,4177,4188,4197,4210,4226,4230,4239,4249,4274,4279,4290,4300,4307,4335,4345,4938,4996,5068,5445,5449,5475,5485,5487,5506,5612,5962,5969 'log-level':4072,4085 'logger':2359,2372,2398,3327,3355,3415,3440,3734,3829,3880,4111,4316,4333,4495,4522,4574,4743 'loginfo':1049,1132,1653,1720,1742,1768,1791,1877 'logrot':4223,5472,5500,5966 'logs.json':4222 'long':156,306 'long-run':155,305 'loop':783,5329,5349,5988 'm':4266,5679 'machin':130,522,2900,2958,2967,3159,3169,3179,3189,3204,3990,4793,5885 'main':2409,2452,2453,3492,3503,3504,3906,3917,3918,4591,4602,4603,5777,5798 'mainpid':404 'manag':436,2462,2470,2503,2509,2514,2517,2520,2565,4933,5268,5486 'mani':3922 'manipul':217 'map':5546 'margin':3312 'match':2892 'max':2559 'maxretentionsec':4155 'maxsiz':4261,4297 'may':2971,5236,5743 'mb':4147 'memori':903 'memoryhigh':914 'memorymax':912,5994 'messag':2319,2377,2426,4057,4370,4376 'metadata':4353 'middlewar':486 'might':5536 'minim':5671 'mirror':984 'miss':791,2059,3234,3531,5166,5319 'missingok':4255,4295 'mission':218,1572,1574,1579,1581,1584,1609 'mission.launch.py':1034 'mission.yaml':1593 'mix':438,631,695,758 'mode':518,2652,2683,2709,2760,2784,2800,2820,5645,5709 'modul':2321,2327,2330,2332,2333,2338 'module_name.replace':2329 'monitor':34,142,2877,2889,3208,3427,3509,3512,3541,3592,3596,3602,3627,3651,3737,4323,4753,5934 'motor':237,590,1018,1281,1284,1288,1291,1295,1325,2534,2722,2764,3604,4394,4398,4636,5541 'motorcontrolnod':5780,5801 'motors.launch.py':1022 'motors.srv':2011 'motors.yaml':1304 'move':5747,5786 'ms':4390 'msg':1133,1721,1743,1769,1792,1878,2301,2335,2344,2370,2423,3426,3692,3698,3748,3778,3898,3904,4557,4563 'msg.status':3435 'msg_type_str.rsplit':2325 'msgs/msg/laserscan':2269,2430 'mt':1408 'multi':129,521,925,2899,2966,3168,5884 'multi-cor':924 'multi-machin':128,520,2898,2965,3167,5883 'multi-user.target':456 'multicast':2970 'multicast/unicast':3052 'multipl':564 'must':424,1988,3222 'n':3693,3697,4845 'n.service':876 'name':116,548,1071,1074,1078,1096,1109,1121,1217,1242,1269,1294,1398,1420,1433,1460,1583,1870,1969,2169,2183,2194,2209,2226,2300,2322,2324,2328,2334,2340,2347,2366,2417,2451,2518,2533,2571,2607,2620,2728,2856,2872,3502,3655,3658,3680,3689,3695,3747,3756,3784,3793,3800,3836,3916,4056,4112,4360,4517,4520,4527,4601 'name1':2149 'name2':2151 'nameserv':2934 'namespac':1401,4361,4363 'nanosecond':4368 'nav':1531,1534,1561,1597 'nav2':1545,1547,1553,1607,2512 'nav2_params.yaml':1543 'navig':216,716,1030,1707,1710,1813,1854,1860,2017,2541,4629,5234,5286,5299 'navigation.launch.py':1033 'necessari':5638 'need':2726 'netplan':2916,2950,2953 'network':2895,2921,2975,3138,4768,5389,5403,5436,6005 'network-online.target':264,325,328,592,595,5380,5414,5440,5442 'network.target':5377,5382,5424,5431 'networkmanager-wait-online.service':5419 'never':5182 'next':2485 'nice':950 'no-pag':5120 'node':45,109,205,888,1180,1207,1208,1215,1234,1235,1241,1261,1262,1268,1285,1286,1293,1320,1322,1324,1326,1355,1387,1410,1448,1450,1451,1459,1491,1522,1575,1576,1610,1949,1961,1968,1987,2143,2148,2150,2168,2176,2182,2192,2193,2207,2208,2223,2225,2231,2285,2295,2442,2449,2455,2463,2474,2479,2497,2510,2524,2532,2547,2555,3245,3265,3273,3288,3359,3494,3497,3499,3510,3513,3545,3561,3585,3597,3603,3624,3628,3652,3661,3665,3679,3683,3688,3694,3741,3746,3755,3783,3787,3792,3799,3835,3908,3911,3913,4068,4081,4089,4114,4271,4311,4327,4328,4330,4358,4380,4410,4412,4447,4458,4466,4497,4586,4593,4596,4598,4630,4704,4707,4714,5228,5235,5622,5729,5731,5773,5779,5782,5800,5804,5815,5935,5939,5953,5999 'node.command':5809 'node.destroy':3498,3912,4597,5814 'node.engage':5812 'node.get':4332 'non':2057,2734 'non-determinist':2733 'non-zero':2056 'notif':3232,3339,3421,3460,5362 'notifempti':4256,4296 'notifi':333,427,600,664,727,3224,3253,3274,3304,3344,3361,3456,4269,5925 'number':2692,2731,2746 'o':5005 'object':223 'on-failur':416,623,687,750,806,5365 'onboard':22,279,3924 'one':211,2486 'onfailur':872 'open':2995,3009 'oper':5498,5626,6032 'orchestr':2456 'order':30,98,179,569,1979,1993,2255,2458,2467,2507,2528,2583,4604,4662,4666,4975,5223,5271,5282,5530,5854,6035 'organ':978 'os':3255 'os.environ.get':3298,3343 'outdoor':1841 'output':541,1100,1112,1124,2521,4005,4041,4052,4220,4320 'overal':3524 'overrid':841 'p':4207 'packag':1209,1236,1263,1287,1402,1413,1426,1452,1577,1962,2511,5053,5167 'packages-select':5052 'pager':5122 'parallel':5279 'param':1532,1535,1559,1562,1598,4098,5562 'paramet':1220,1244,1271,1297,1435,1462,1586,2523,3594,3601,3612,3618,3626,3632,3640,4095,5615 'parser':2410 'parser.add':2412,2421,2431 'parser.parse':2439 'particip':3028 'partof':660,723,4610,4643 'pass':1136,5807 'password':2946 'path':510,2612,2769,2855,5515,5559 'pathjoinsubstitut':1176,1221,1298,1351,1436,1463,1518,1537,1551,1587,1670,1727,1749,1775,1798,1937 'pattern':4,2240,5130 'payload':1840,1957,1978 'peer':2962,5880 'per':496,3967,4142 'per-fil':4141 'per-servic':3966 'percept':90,221,650,705,1327,1393,1395,1399,1488,1702,1705,1761,1764,1771,1783,1844,1850,1889,2015,4650,5848 'perception.launch.py':685,1023,1779 'perform':3946,6025 'period':1129,2553,3223,3367,3620,3638,3642,3725,3731 'peripher':281 'permiss':947,5608,5640 'persist':4125,4130,5616 'pet':3941 'physic':2613 'pid':4879,4883,4885,4928 'pipefail':2063,2154,4802,5029 'pipelin':1394 'pkg':1685,1728,1750,1776,1799,4066,4079 'place':291 'plan':219,717,1031,2018 'planner':1573,1582,1585 'plug/unplug':2883 'plugdev':5688 'plugin':1416,1429 'point':226,1626,2944,5584 'polici':773,5977 'poll':2072,2116,2120,2164,2215,2219 'port':1246,1272,2611,2629,2660,2768,2989,2997,3011,3023,3035,3044,3063,3065,3067,3075,3077,3079,3091,3093,3095,4817,4832,4834,4836,4843,4848,4851,4854,4858,4862,4866,4870,4874,5564,5581,6013 'postrot':4268 'power':277,6028 'power-off':6027 'practic':7 'pre':352 'pre-start':351 'presenc':5911 'present':1088,2046,2133 'press':4920 'prevent':781,885,4624,5986,5997 'prioriti':935,4199 'privatetmp':968 'privileg':5672 'problem':5138,5224,5311,5381,5447,5517,5596,5718 'proc':1415,1418 'process':435,3221,3279,5931 'product':33,144,190,4302,4322 'profil':2290,2350 'properti':2894 'protecthom':959 'protectsystem':963 'proto':3056,3084 'pub':3703,3710,3719,4469,4476,4716 'publish':1278,2252,3523,3701,3705,3712,3721,3766,3889,3894,4471,4478,4718 'pushrosnamespac':1675 'python':1038,1152,1330,1495,1639,1912,2243,2260,2488,3239,3537,4303,4427,4691,5768 'python3':2262,2265,3241,3539,4429 'pythonexpress':1671,1904,1921,1944,1974 'pythonlaunchdescriptionsourc':1171,1513,1550,1665,1726,1748,1774,1797 'q':2181 'qos':2289,2349,3670,3700,3716 'qosprofil':3565,3671 'queri':2854 'r':5678 'race':105,184,5231 'rais':2387,2405 'ramp':824 'rang':2986,4547 'rate':778,1279,4157,5309,5357,5979 'rate-limit':5356,5978 'ratelimitburst':4165 'ratelimitintervalsec':4163 'rather':468 'rcl':4109 'rclcpp':1403 'rclpi':2281,3261,3557,4443,4693,4700,5759 'rclpy.init':2441,3493,3907,4592,5778,5799 'rclpy.node':2283,3263,3559,4309,4445,4702 'rclpy.qos':2287,3563 'rclpy.shutdown':3500,3914,4587,4599,4728,4741,5816 'rclpy.spin':2448,3496,3910,4595,5781,5803 'rcutil':534,539,4027,4039,4050 'reach':5384 'read':961,3289 'read-on':960 'readi':1013,1127,1138,1148,2035,3352 'readwritepath':965 'real':932,1633,1827,1897,2881 'real-tim':931 'realsens':2689 'realtim':953 'rear':2656 'reboot':2832,3949,3987,4128,4962,5533,5573 'rebootwatchdogsec':3960 'receiv':2378,2789,4525,4572 'recept':3751 'record':3749 'recoveri':776,848,6037 'rectifi':1421 'rectifynod':1419 'regist':4482,4722,5754,5949 'relat':4663 'releas':5051,5096 'reli':5171,5375 'reliabilitypolici':3566 'reliabilitypolicy.reliable':3673 'reliabl':3669,3672,3699,3715 'reload':2824,2828,2837,3114 'reload-rul':2836 'remap':1228,1422 'remot':4171,4752,4929,4939 'reopen':4273 'requir':329,596,658,721,941,2042,2064,2080,2130,2230,2960,5260,5290,5302,5853 'reset':247,861 'reset-fail':860 'resourc':877,882,5990 'respawn':2558 'rest':892 'restart':407,415,573,622,686,749,772,782,805,815,826,3236,3978,4957,4968,5021,5098,5106,5306,5325,5343,5348,5351,5359,5364,5976,5987 'restartsec':419,626,690,753,809,831,842,5353,5368 'restrict':956 'retri':846,865 'return':1139,1305,1474,1594,1814,2562,3450,3467 'reusabl':2242 'revers':4659,5544 'risk':5604 'rmw':255,488,490,5201,5203,5864 'robot':2,16,20,41,49,59,134,145,158,186,191,316,335,337,388,452,497,544,547,550,552,561,584,602,619,640,649,666,683,704,713,729,746,767,803,874,1040,1070,1073,1077,1082,1154,1224,1301,1332,1439,1466,1497,1540,1590,1636,1641,1689,1696,1699,1830,1836,1881,1883,1898,1923,1926,1940,1945,1975,2911,3001,3102,3134,3923,4181,4259,4260,4649,4653,4774,4795,4798,4803,4808,4876,4937,4941,4951,4964,4977,4990,5000,5019,5025,5030,5035,5056,5061,5070,5077,5079,5099,5102,5113,5126,5285,5298,5632,5674,5682,5689,5695,5697,5705,5742,5783,5817,5899 'robot-alert':873 'robot-appl':766,4652 'robot-bringup':1,451 'robot-bringup.service':4194,4206,4215,4284,4945,5004 'robot-bringup.target':266,644,708,771,5107,5119 'robot-driv':639 'robot-drivers.service':657,659,661,863,4185,4955 'robot-hw.target':265,326,330,593,597 'robot-percept':703,4648 'robot-perception.service':720,722,724,4644,4646,4956,4969,5301,5303 'robot-specif':543 'robot/camera_depth':2708 'robot/camera_front':2651 'robot/camera_rear':2682 'robot/gps':2799 'robot/imu':2759 'robot/lidar':2819 'robot/motor_controller':2783 'robotnetwork':2945 'root':5595,5600,5651,5661 'ros':256,379,482,503,524,530,610,674,737,2157,3125,3129,3145,3162,3172,3181,4070,4083,4238,4812,4898,4905,5085,5198,5217,5862,5866,5968,6016 'ros-arg':4069,4082 'ros2':14,44,65,78,131,250,286,288,300,317,341,363,385,400,480,616,680,743,1238,2142,2175,2901,3119,3190,3193,3196,3244,3278,3506,4017,4063,4076,4097,4105,4120,4225,4228,4270,4315,4409,4411,4671,4759,4788,4828,4890,4912,4915,5157,5227,5448,5474,5561,5598,5621,5657,5728,5856,5930 'ros2-based':13 'ros2-level':3505 'rosbag2':5611 'rotat':150,4016,4245,4251,4292,5446,5963 'rout':2929 'rsync':5062 'rtprio':946 'rule':112,243,2567,2574,2593,2715,2830,2838,2844,2982,5550,5579,5646,5699,5822,6014 'run':157,307,4064,4077,4726,4903,5590,5597,5649,5659 'runaway':887,5998 'runtim':1910,4062 'runtimewatchdogsec':3958,3986 'safe':169,3535,3548,3869,3883,4423,4435,4464,4514,4529,4576,4687,5766,5795,5943 'safe_shutdown_node.py':4430 'safeshutdownnod':4457,4594 'safeti':3311 'schedul':934 'screen':1101,1113,1125,2522 'script':2021,2025,5904 'sd':426,3252,3455,5924 'sec':3314,3324,3364,3374,3615,3621,3635,3643,3844,3852 'second':819 'section':800,901,5507 'secur':5603 'securepassword':2947 'sedp':3072 'see':2886,3200,5183,5505 'segment':2976 'select':487,1638,5054 'self':2298,2369,2384,3284,3397,3425,3457,3588,3745,3761,3871,4326,4337,4383,4461,4506,4710,4737 'self._callback':2348 'self._check_health':3732 'self._check_timeout':2357 'self._diag_callback':3389 'self._heartbeat_callback':3696 'self._on_shutdown':4733 'self._sd_notify':3351,3410 'self._shutdown_handler':4490,4493 'self._trigger_safe_stop':3866 'self._watchdog_tick':3375 'self.brake':4475 'self.brake_pub.publish':4561 'self.cmd':3717,4467,4715 'self.cmd_pub.publish':4750 'self.cmd_vel_pub.publish':3892,4549 'self.context':4730 'self.create':2342,2354,3371,3385,3684,3704,3711,3720,3728,4470,4477,4717 'self.declare':3600,3611,3617 'self.destroy':4585 'self.diag':3383 'self.estop':3709 'self.estop_pub.publish':3902 'self.get':2358,2371,2397,3326,3354,3414,3439,3625,3631,3639,3733,3774,3828,3879,4494,4521,4573,4742 'self.health':3702 'self.health_pub.publish':3859 'self.last':3653,3753,3790 'self.logger':4331,4374 'self.monitored':3623,3660,3682,3740,3786 'self.node':4329 'self.node.get':4359,4362,4365 'self.notify':3341,3465,3469 'self.received':2309,2379,2386 'self.slog':4381 'self.slog.log':4384,4392 'self.start':2313,2392 'self.sub':2341 'self.system':3391,3408,3447,3451 'self.timeout':2311,2396,2403,3630,3803,3855 'self.timer':2353 'send':393,3368,3398,3459,3872,4631,5721 'sendsigkil':4685 'sensor':229,654,2268,2291,2351,2429 'sequenc':197,2476,4405 'serial':121,1245,1248,2691,2702,2717,2719,2741,2745,2756,2816,2868,5525,5563,5580,5832 'servic':26,178,263,283,289,292,308,331,423,558,566,598,662,725,786,799,804,900,987,2006,2030,3218,3238,3968,3980,4106,4121,4182,4619,4667,4932,4948,4974,4988,5022,5100,5110,5140,5155,5176,5190,5208,5241,5276,5313,5350,5363,5692,5841,5921,6034 'set':81,96,147,254,835,930,1864,2061,2152,3215,3332,4021,4058,4091,4099,4800,4823,4886,5027,5149,5478,5992,6019 'setparamet':1676,1869 'setremap':1181 'setup':458,523,2968,3170 'setup.bash':5133 'sever':4054 'share':1686,1729,1751,1777,1800 'shut':4579 'shutdown':162,392,4404,4433,4465,4487,4501,4504,4510,4530,4605,4690,4695,4732,4736,4745,5716,5761,5771,5792,5947 'shutdownwatchdogsec':3962 'sig':4516,4526 'sigint':394,440,633,697,760,4415,4669,4680,5724,5959 'sigkil':442,4678 'signal':3346,3878,4282,4406,4439,4483,4511,4673,5755 'signal.sigint':4492 'signal.signal':4488,4491 'signal.signals':4518 'signal.sigterm':4489 'signum':4507,4519 'sigterm':4417,4676,4684,5722,5960 'silent':3553 'sim':1191,1194,1309,1564,1568,1603,1692,1695,1735,1757,1759,1806,1808,1818,1866,1872,1876,1895,4102 'sim/real':5897 'simpler':3104 'simul':1631,1715,1824,6039 'simultan':5229 'sinc':2729,2969,3822,4189,4216 'singl':94,1624,3158,4959 'single-machin':3157 'situat':4626 'size':4134,4144,5480 'skill':37,51,56 'skill-robot-bringup' 'skip':1713 'slam':225,652,1024,1371,1374,1447,1449,1453,1457,1461,1473,1478,1490,2016,2540,3610 'slam.launch.py':1027 'slam.yaml':1469 'sleep':2115,2214,5115 'sllidar':1237,1240 'sock':3478 'sock.close':3490 'sock.connect':3485 'sock.sendall':3487 'socket':3257,3340,3342,3345,3466,3470 'socket.af':3480 'socket.sock':3482 'socket.socket':3479 'softwar':3938 'soon':5386 'sourc':77,252,371,377,382,470,608,613,672,677,735,740,1169,1511,1663,2155,2160,5083,5132,5178,5193,5215,5655 'source-arpitg1304' 'spdp':3020,3051 'specif':545,1932,1992,2847,4180 'specifi':2141,5666 'split':559 'src':5069 'ssh':4756,4765,4785,4825,4844,4878,4881,4884,4927,4940,4950,4963,4976,4989,4999,5011,5078,5101,5112 'ssh-ros2-tunnel.sh':4782 'stabl':2596,2605,2727,5553,5575,5835 'stack':66,188,319,562,651,1851,1863,5593 'stamp':4364 'standard':1834,1838 'standarderror':448 'standardoutput':446,636,700,763 'start':43,62,298,353,361,1722,1744,1770,1793,1989,2048,2487,3195,3360,4498,4665,5225,5277,5391,5913 'startlimitburst':822,850,5340,5372,5984 'startlimitintervalsec':820,5338,5370,5982 'startup':31,99,196,1980,1984,2031,2256,2459,2466,3348,4162,5222,5254,5317 'startup/shutdown':2506 'starv':890,6001 'state':170,857,3458,4424,4436,4515,4577,4688,5767,5796 'state.encode':3488 'static':2902,2907,6010 'station':137,3005 'status':3433,3794,3858,4949,4954,5111,5118 'status.level':3437,3804,3814 'status.message':3446,3806,3816 'status.name':3445,3796 'status.values':3840 'std_msgs.msg':3569,4453 'stderr':4034 'stdout':537,4030 'step':4531,4552,4564,4581 'still':5785 'stop':3192,3536,3549,3870,3877,3884,3896,4621,4651,4656,4661,5726,5944 'storag':4129,5469 'store':461 'str':2303,4340,4342 'stream':4994 'strict':964 'string':3572 'structur':152,4299,4318,4347,4995 'structuredlogg':4313,4382 'sub':3384 'subnet':3103 'subscrib':1996,3376,3515,3662 'subscript':2343,3386,3685 'subsystem':2635,2666,2693,2747,2770,2790,2807,2891,3521 'subsystem-match':2890 'sudo':2833,2839,2849,2952,3053,3081,3106,3112,4001,4010,4966,5104,5676,5683 'super':2305,3285,3589,4462,4711 'suppli':278 'support':311 'swap':1906 'symlink':2597,2650,2681,2707,2758,2782,2798,2818,5554,5576,5836 'sync':5059 'sys':2275,4441 'sys.exit':4588 'syslog':4169 'syslogidentifi':450,638,702,765 'system':17,146,895,927,3378,3404,3417,3525,5466,5627,6003 'systemctl':859,4280,4953,4967,4992,5105,5117 'systemd':25,70,72,261,269,273,282,421,460,477,565,986,3209,3225,3235,3249,3275,3294,3338,3402,3462,3956,3976,3992,4607,4980,5137,5139,5175,5180,5262,5720,5840 'systemd-analyz':4979 'systemd-networkd-wait-online.service':5417 'systemexit':2388,2406 'systemmaxfiles':4148 'systemmaxus':4138,5510,5973 'talk':502 'target':262 'tco':3933 'templat':297 'test':999,1093,1106,1118,2826,2842,2851,5825,6024 'thrash':830 'tick':3396 'tight':5328 'time':176,933,938,1565,1867,1873,2277,2314,2393,2882,3259,3555,3648,3752,4055,4103,4973 'time-crit':937 'time.time':2315,2391,3656,3757,3769 'timeout':2070,2092,2101,2113,2162,2188,2198,2212,2270,2304,2312,2383,2401,2433,2544,2550,3599,3614,3634,3818,3833,3851,4009 'timeoutstartsec':443 'timeoutstopsec':405,634,698,761,4681 'timer':2355,3372,3729,3930 'timeract':1051,1128,1654 'toolbox':1454,1458 'top':1004,1612 'top-level':1003,1611 'topic':1998,2239,2249,2299,2307,2346,2364,2365,2374,2414,2416,3197,3201,3518,3668,4760,4913,4916 'topic-agent-skills' 'topic-ai-coding-assistant' 'topic-claude-skills' 'topic-robotics' 'topicwait':2294,2443 'total':4132 'track':3645 'traffic':3074,3099,3151,4763,4790 'transit':2526,2548,4437,5272 'tri':3484,5802 'trigger':869,2841,3533,3547,3868,3882,3975,5942 'true':969,1257,1376,1383,1481,1487,1847,1857,2380,2543,2941,3393,3453,3781,3901,4287,4560 'tti':2748,2771,2791,2808,2893 'tunnel':4757,4786,4826,4882,4925 'twist':3576,3722,3893,4451,4472,4538,4539,4551,4719,4751 'type':332,599,663,726,2302,2320,2336,2345,2424,2427,2434,2446,4339,4357,5050,5095 'u':4184,4193,4205,4214,4944,5003 'ubuntu':2917 'udev':111,242,1010,2566,2573,2592,2714,2829,2878,5549,5578,5642,5698,5821 'udevadm':2618,2834,2840,2850,2852,2870,2888 'udp':2988,3022,3057,3085 'ufw':3054,3082,3107,3113 'unbound':4243 'unhealthi':3418 'uniqu':495 'unit':73,284,290,314,582,647,711,854,3219,3971,4642,5191,5209,5263,5283,5296,5429,5438,5693,5842,5922 'unix':3481 'unlesscondit':1659,1733,1902,1917 'unpredict':2590 'unrespons':3995 'updat':5009 'uri':512,5871 'usag':883,2146,2264,4377,4796,4806,5023,5033 'usb':246,1210,1213,2577,2610,2627,2658,2767,5524,5828 'use':35,54,536,858,1190,1193,1308,1384,1563,1567,1602,1691,1694,1734,1756,1758,1805,1807,1817,1823,1865,1871,1875,1900,2022,2253,2460,2591,2864,2915,2984,3021,3155,3333,4029,4101,4277,4668,4692,4892,4911,5186,5257,5265,5305,5337,5413,5548,5758 'usec':3297,3300,3303,3317,3330 'user':334,601,665,728,943,3073,5633,5664,5669,5675,5694,5706 'useradd':5677 'usermod':5684 'util':2244 'valu':1080,1311,1317,1480,1486,1600,1605,1820,1833,1846,1856,1874,3629,3636,3644,3845,3853,4352 'variabl':343,463,5148 'variant':1637,1697,1700,1831,1837,1882,1884,1899,1924,1927,1931,1935,1943,1946,1958,1976,5900 'variant-specif':1930 'vel':3718,4468 'veloc':3874,3891,4535,4632,4749,5737,5752,5790,5811,5955 'verifi':355,2039,3185,3996,5909,6033 'version':2922 'via':69,366,837,2932,3251,3985,4024,4094,4606,5010,5577,5641,5965,5993 'video':2633,2655,2664,2686,2712,5687 'video4linux':2636,2667,2694 'view':2858,4176,4187,4196,4935,4970 'vs':1632,1896 'w':48 'wait':2108,2205,2237,2362,2419,4566,4926,5433 'wait-for-ros2-nodes.sh':2147 'wait-for-top':2236 'wait_for_topic.py':2263,2266 'waiter':2308 'walk':2624,2876 'want':327,594,5441 'wantedbi':455,643,707,770 'warn':3328,3881,4523 'watchdog':139,310,422,428,3205,3210,3250,3287,3291,3296,3299,3302,3308,3316,3329,3358,3369,3395,3399,3411,3420,3920,3929,3943,3954,3983,3998,4008,5916,5926 'watchdog_node.py':3242 'watchdognod':3272,3495 'watchdogsec':433,628,692,755,3213,3973,5919 'wdt':3935 'week':5496 'wifi':2938 'wire':2913 'withhold':3419 'within':430,817 'without':2831,4244,4961,5308,5457,5734 'wlan0':2939 'work':2973 'workingdirectori':338 'workspac':79,253,373,5037,5041,5072,5082 'wrap':4314 'write':71,110,160,2572,2713,4229 'written':5823,5905 'xml':2977,5876 'y':4265 'yaml':1947,2919,5555 'yes':4174,4686 'yet':2002,5245,5411 'zero':1391,2058,3873,3890,4534,4537,4550,4748,5736,5810,5954 'zero-copi':1390","prices":[{"id":"7f541cf6-c196-4543-ac7c-de1a01043031","listingId":"dfb02537-ed3d-4347-8017-f284f665678c","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"arpitg1304","category":"robotics-agent-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T22:05:33.419Z"}],"sources":[{"listingId":"dfb02537-ed3d-4347-8017-f284f665678c","source":"github","sourceId":"arpitg1304/robotics-agent-skills/robot-bringup","sourceUrl":"https://github.com/arpitg1304/robotics-agent-skills/tree/main/skills/robot-bringup","isPrimary":false,"firstSeenAt":"2026-04-18T22:05:33.419Z","lastSeenAt":"2026-05-02T18:54:20.708Z"}],"details":{"listingId":"dfb02537-ed3d-4347-8017-f284f665678c","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"arpitg1304","slug":"robot-bringup","github":{"repo":"arpitg1304/robotics-agent-skills","stars":189,"topics":["agent-skills","ai-coding-assistant","claude-skills","robotics"],"license":"apache-2.0","html_url":"https://github.com/arpitg1304/robotics-agent-skills","pushed_at":"2026-03-25T03:44:12Z","description":"Agent skills that make AI coding assistants write production-grade robotics software. ROS1, ROS2, design patterns, SOLID principles, and testing — for Claude Code, Cursor, Copilot, and any SKILL.md-compatible agent.","skill_md_sha":"ecb7dc08a0238a645143545278aa1aa51d740951","skill_md_path":"skills/robot-bringup/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/arpitg1304/robotics-agent-skills/tree/main/skills/robot-bringup"},"layout":"multi","source":"github","category":"robotics-agent-skills","frontmatter":{"name":"robot-bringup","description":"Patterns and best practices for bringing up a complete ROS2-based robotics system on a robot's onboard computer, including systemd services, launch file composition, ordered startup, and production monitoring. Use this skill when configuring a robot to start ROS2 nodes on boot, writing systemd unit files for ROS2 launch, composing layered launch files for full robot stacks, setting up watchdog monitoring, configuring udev rules for deterministic device naming, or debugging boot-time race conditions. Trigger whenever the user mentions robot bringup, robot startup, systemd for ROS2, ROS2 on boot, launch file composition, robot boot sequence, udev rules for cameras or serial ports, watchdog for robot systems, automatic restart for ROS2 nodes, network configuration for multi-machine ROS2, log rotation for robots, graceful shutdown of robot stacks, or SSH-based remote debugging of robots. Also trigger for environment setup in systemd (sourcing workspaces), ordered startup with health checks, deterministic device naming, or any discussion of running ROS2 systems as long-running production services. Covers systemd on Ubuntu 22.04/24.04 with ROS2 Humble, Iron, and Jazzy."},"skills_sh_url":"https://skills.sh/arpitg1304/robotics-agent-skills/robot-bringup"},"updatedAt":"2026-05-02T18:54:20.708Z"}}