Developer Guide - Forestry-Robotics-UC/fruc_dataset_apparatus GitHub Wiki

Developer Manual

Introduction

This guide is for developers who need to:

  • Modify or extend the FRUC Dataset Apparatus system
  • Add new sensors or recording capabilities
  • Debug and maintain the codebase
  • Contribute improvements to the project

Prerequisites

Required Knowledge

  • Familiarity with ROS2 concepts (nodes, topics, services, launch files)
  • Basic Docker and containerization knowledge
  • Linux command-line proficiency
  • C++ or Python programming skills

Required Tools

  • Docker and Docker Compose
  • Git and version control basics
  • A text editor or IDE (VSCode recommended)
  • ROS2 Jazzy development environment (optional, but useful)

Project Structure

Directory Organization

fruc_dataset_apparatus/
├── .github/              # GitHub workflows and CI/CD
├── .git/                 # Git repository data
├── .gitignore           # Git ignore rules
├── .names_01, .names_02 # Random dataset naming files
├── README.md            # Main readme
│
├── docker/              # Docker configurations
│   ├── Dockerfile.*     # Individual container definitions
│   ├── docker-compose.yml # Orchestration configuration
│   ├── docker-compose.link-local.yml # Network variant
│   ├── pre_recording_test.sh # Automated testing script
│   │
│   ├── docker_build/    # Build-time resources
│   │   ├── entrypoint.sh # Container startup script
│   │   ├── apparatus_description/ # Robot URDF package
│   │   │   ├── launch/  # ROS2 launch files
│   │   │   ├── urdf/    # Robot model files
│   │   │   ├── config/  # Configuration files
│   │   │   └── meshes/  # 3D mesh files (STL, OBJ)
│   │   └── rslaunch/    # RealSense custom launch files
│   │
│   └── docker_shared/   # Runtime shared volumes
│       ├── scripts/     # Sensor launch scripts
│       │   ├── xsens-launch.sh
│       │   ├── realsense-launch.sh
│       │   ├── emlid-launch.sh
│       │   ├── ouster.launch.py
│       │   ├── mtnode.py
│       │   └── mtdevice.py
│       └── ouster_config.yaml # Ouster settings
│
├── realsense-udev-rules/  # Device permission rules
│   ├── realsense-udev-install.sh
│   └── README.md
│
├── rviz/                # RViz visualization configs
│   ├── 435i-udrf.rviz
│   ├── realsense.rviz
│   └── *.rviz files
│
├── rosbags/             # Recording output directory
│   └── [timestamp]__[name]/ # Individual recordings
│
├── launch-system.sh     # Main launch script
├── gnome-launch-system.sh # GNOME variant
├── stop-system.sh       # System shutdown script
├── gnome-stop-system.sh # GNOME variant
└── tty-perms.sh         # Serial port permissions

fruc_dataset_apparatus.wiki/
├── Home.md              # Wiki home page
├── Technical-Architecture.md (NEW)
├── Tools-Libraries-Dependencies.md (NEW)
├── Developer-Guide.md (THIS FILE - NEW)
├── Installation-Setup.md (NEW)
├── User-Manual.md (NEW)
├── Scripts-Reference.md (NEW)
└── [existing wiki files]

Adding a New Sensor

Step 1: Create Dockerfile

Create a new Dockerfile in docker/Dockerfile.newimenor:

# NEW SENSOR
FROM ros:jazzy-ros-base

# Install base dependencies
RUN apt update && apt install -y --no-install-recommends \
    build-essential \
    git \
    python3-colcon-common-extensions \
    python3-pip \
    python3-rosdep

# Create workspace
WORKDIR /docker_ws/src/

# Clone the sensor driver repository
RUN git clone https://github.com/example/example-ros-driver.git

# Install ROS dependencies
RUN rosdep update && rosdep install --from-paths . -y --ignore-src

# Build workspace
WORKDIR /docker_ws
RUN . /opt/ros/jazzy/setup.sh && colcon build --packages-up-to example_driver

# Copy entrypoint and make executable
COPY ./docker_build/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Set container defaults
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "ros2", "launch", "example_driver", "example_driver.launch.xml" ]

Step 2: Update docker-compose.yml

Add your service to docker/docker-compose.yml:

  newsensor:
    build:
      context: .
      dockerfile: Dockerfile.newimenor
    image: ros2-apparatus-newimenor:jazzy
    stdin_open: true
    tty: true
    privileged: true
    container_name: ros2-apparatus-newimenor
    networks:
      - ros2-net
    devices:
      - /dev/ttyUSB1:/dev/ttyUSB1  # If using serial
    volumes:
      - ./docker_shared:/shared
    command:
      - ros2
      - launch
      - example_driver
      - example_driver.launch.xml
      - frame_id:=newimenor_sensor

Step 3: Update Launch Scripts

Modify launch-system.sh to include your sensor's topics:

# In the topics checklist section, add:
/newsensor/data "New Sensor Data" on \
/newsensor/status "New Sensor Status" off \

Step 4: Update Recording Container (if needed)

If your sensor requires special dependencies, update Dockerfile.recording:

RUN git clone https://github.com/example/example-driver.git

Working with ROS2 Launch Files

Creating Custom Launch Files

Create launch files in Python format (.launch.py):

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        DeclareLaunchArgument(
            name='frame_id',
            default_value='sensor_frame',
            description='TF frame ID for the sensor'
        ),
        DeclareLaunchArgument(
            name='use_sim_time',
            default_value='False',
            description='Use simulation time'
        ),
        Node(
            package='sensor_driver_pkg',
            executable='sensor_driver_node',
            name='sensor_driver',
            parameters=[{
                'frame_id': LaunchConfiguration('frame_id'),
                'use_sim_time': LaunchConfiguration('use_sim_time'),
            }],
            remappings=[],
        ),
    ])

Launch File Best Practices

  1. Always use LaunchConfiguration for parametrization
  2. Declare all arguments at the top with defaults
  3. Use meaningful names for frame IDs and namespaces
  4. Document parameters in description fields
  5. Handle remappings for topic namespace consistency

Modifying Launch System Scripts

launch-system.sh Structure

The main orchestration script follows this pattern:

#!/bin/bash

# 1. SELECT RECORDING NAME
#    - Generates default name from timestamp + random adjectives
#    - Uses kdialog for user input

# 2. SELECT TOPICS FOR RECORDING
#    - Presents kdialog checklist
#    - Each line: /topic_name "Display Name" on/off

# 3. SELECT BAG SPLIT STRATEGY
#    - Dialog to choose between size-based or time-based splitting

# 4. SELECT COMPRESSION PROFILE
#    - Options: none, fastwrite, zstd_fast, zstd_small

# 5. CONFIRMATION
#    - Displays all selected options
#    - User confirms to proceed

# 6. LAUNCH SYSTEM
#    - Starts Docker containers
#    - Begins recording

Building Custom Containers

Local Build and Test

For Desktop (Docker):

# Navigate to docker directory
cd docker/

# Build a specific container
docker build -f Dockerfile.newimenor -t ros2-apparatus-newimenor:jazzy .

# Or use docker-compose 
docker-compose build newimenor

# Test the container
docker-compose up -d newimenor
docker exec ros2-apparatus-newimenor ros2 topic list
docker-compose down

For SteamDeck (Podman):

# Navigate to docker directory
cd docker/

# Build a specific container
podman build -f Dockerfile.newimenor -t ros2-apparatus-newimenor:jazzy .

# Or use podman-compose 
podman-compose build newimenor

# Test the container
podman-compose up -d newimenor
podman exec ros2-apparatus-newimenor ros2 topic list
podman-compose down

Docker vs Podman Compatibility

Both Docker and Podman use the same command syntax. Simply replace docker with podman and docker-compose with podman-compose:

Operation Docker Podman
Build image docker build ... podman build ...
Compose commands docker-compose ... podman-compose ...
Execute in container docker exec ... podman exec ...
View logs docker logs ... podman logs ...
Interactive shell docker exec -it ... podman exec -it ...

Debugging Container Issues

For Desktop (Docker):

# Interactive shell in running container
docker exec -it ros2-apparatus-newimenor bash

# View container logs
docker logs ros2-apparatus-newimenor

# Rebuild with no cache
docker-compose build --no-cache newimenor

# Verbose build output
docker-compose build newimenor --verbose

For SteamDeck (Podman):

# Interactive shell in running container
podman exec -it ros2-apparatus-newimenor bash

# View container logs
podman logs ros2-apparatus-newimenor

# Rebuild with no cache
podman-compose build --no-cache newimenor

# Verbose build output
podman-compose build newimenor --verbose

Working with the Recording Container

Recording Implementation

The recording system uses two approaches:

1. hector_recorder (Primary)

ros2 run hector_recorder record \
  -b 5000000000 \           # Size limit in bytes
  --topics /topic1 /topic2 \
  -o /rosbags/recording_name

Advantages:

  • Advanced filtering
  • Custom compression
  • Integration with ROS tools

2. ros2 bag record (Fallback)

ros2 bag record \
  --storage-preset-profile zstd_fast \
  -d 60 \                   # Duration in seconds
  --topics /topic1 /topic2 \
  -o /rosbags/recording_name

Advantages:

  • Standard ROS2 tool
  • Well-tested
  • Direct MCAP format support

Modifying Recording Settings

Edit launch-system.sh to change defaults:

# Default bag limit
bag_limit_value=$(kdialog --inputbox "Enter max bag size (GB):" "5")

# Default compression (in docker-compose)
storage_profile="zstd_fast"

Robot Description Package

URDF File Organization

Located in docker_build/apparatus_description/urdf/:

  • robot.urdf or robot.urdf.xacro - Main robot model
  • sensors.urdf.xacro - Sensor mount definitions
  • materials.urdf.xacro - Visual properties

Adding Sensor Transforms

Edit URDF to add new sensor frame:

<!-- New Sensor Joint -->
<joint name="base_to_newsensor" type="fixed">
  <parent link="base_link"/>
  <child link="newsensor_link"/>
  <origin xyz="0.1 0 0.05" rpy="0 0 0"/>
</joint>

<link name="newsensor_link">
  <visual>
    <geometry>
      <mesh filename="package://apparatus_description/meshes/newsensor.stl"/>
    </geometry>
  </visual>
</link>

Building and Testing URDF

# From docker_build/apparatus_description/
python3 setup.py build

# Test URDF validity
check_urdf file.urdf

# View in RViz
ros2 launch apparatus_description view_robot.launch.py

Debugging Tips

Topic Monitoring

# List all active topics
ros2 topic list

# Check topic rate and type
ros2 topic hz /topic_name
ros2 topic type /topic_name

# Echo topic data
ros2 topic echo /topic_name

# Monitor all activity from a container
docker exec ros2-apparatus-xsens ros2 topic list -v

Frame Debugging

# List TF tree
ros2 run tf2_tools view_frames

# Monitor specific transform
ros2 run tf2_ros tf2_echo base_link camera_link

# Check for transform gaps
ros2 run ros2_control_demo_example_12 static_broadcaster --frame-id base_link

Performance Analysis

# Monitor container resource usage
docker stats ros2-apparatus-realsense

# Check ROS2 middleware settings
echo $ROS_MIDDLEWARE_IDENTIFIER

# Profile topic message rates
ros2 run ros2_tracing tracetools_launch example_hooks

Continuous Integration

GitHub Workflows

The project uses .github/workflows/ for CI/CD:

  • Automated Docker builds
  • Container testing
  • Documentation generation
  • Release automation

Local Testing Before Commit

# Verify Dockerfiles
docker-compose config

# Test composition startup
docker-compose up -d
sleep 10
ros2 topic list
docker-compose down

# Verify shell scripts
bash -n launch-system.sh  # Check syntax
shellcheck launch-system.sh  # Lint check

Contributing Changes

Git Workflow

  1. Create feature branch

    git checkout -b feature/add-new-sensor
  2. Make changes

    • Update Dockerfiles, scripts, launch files
    • Test locally
    • Document in code
  3. Commit with clear messages

    git commit -m "feat: add newimenor sensor driver"
  4. Push and create pull request

    git push origin feature/add-new-sensor

Code Style Guidelines

  • Bash scripts: Use bash -n for syntax checking
  • Python: Follow PEP 8, use type hints where possible
  • ROS2 launch files: Use consistent parameter naming
  • Comments: Explain "why", not just "what"

Extending Recording Capabilities

Custom Recording Node

Create a ROS2 node in docker_build/ to handle specialized recording:

import rclpy
from rclpy.node import Node
from rosbag2_py import SequentialWriter

class CustomRecorder(Node):
    def __init__(self):
        super().__init__('custom_recorder')
        self.writer = SequentialWriter()
        
        # Subscribe to topics
        self.create_subscription(
            Imu, '/imu/data',
            self.imu_callback, 10)
    
    def imu_callback(self, msg):
        # Custom processing before recording
        self.writer.write('imu_data', msg, rclpy.clock.Clock().now())

Post-Processing Pipeline

Add hooks in recording container for automatic conversion:

# In docker/docker_build/entrypoint.sh
# After recording completes:

# Convert MCAP to legacy rosbag format
rosbag2 convert -i input.mcap -o output

# Generate metadata
python3 analyze_rosbag.py input.mcap > metadata.json

Performance Optimization

Docker Optimization

  1. Use .dockerignore to exclude unnecessary files
  2. Multi-stage builds to reduce image size
  3. Layer caching - put frequently changing items last

Recording Optimization

  1. Compression profiles: Trade-off speed vs. size
  2. Topic filtering: Record only needed data
  3. QoS settings: Adjust message reliability

Network Optimization

For distributed sensors, consider:

  • Static IP assignment (link-local networks)
  • Separate network bridges for high-bandwidth sensors
  • DDS participant isolation via ROS_DOMAIN_ID

Troubleshooting Common Issues

Build Failures

Problem: Dockerfile build fails
Solution:

docker-compose build --no-cache --verbose sensor_name
# Check for missing dependencies in ROS repositories
rosdep search package_name

Problem: rosdep can't find dependencies
Solution:

rosdep update  # Update the database
rosdep keys --all  # List all available keys

Runtime Issues

Problem: Sensor not publishing data
Solution:

# Check device permissions
ls -la /dev/ttyUSB*

# Reinstall udev rules
sudo ./realsense-udev-rules/realsense-udev-install.sh

# Restart container with privileged mode
docker-compose down
docker-compose up -d

Problem: ROS topics not visible between containers
Solution:

# Verify network connectivity
docker-compose exec sensor_container ping publisher

# Check ROS_DOMAIN_ID
docker-compose exec sensor_container env | grep ROS_

# View network configuration
docker network inspect docker_ros2-net

Resources


Last Updated: 2026-03-30
Related Documentation: Technical Architecture, Tools and Libraries

⚠️ **GitHub.com Fallback** ⚠️