Skip to main content

Creating Modules

This guide walks through creating a new module from scratch using the SDK’s convention-over-configuration approach.

Module Structure

Every module follows this structure:
modules/MyModule/
├── config/
│   └── My.yaml           # Module configuration
├── src/
│   ├── MyTask.hpp        # Task header
│   └── MyTask.cpp        # Task implementation
├── build/
│   └── generated/
│       └── MyTask.generated.hpp  # Auto-generated
└── CMakeLists.txt

Step-by-Step Guide

Step 1: Create Directory Structure

# Create module directory (PascalCase)
mkdir -p modules/MyNewModule/{config,src,build}
cd modules/MyNewModule

Step 2: Create Configuration

Create config/MyNew.yaml:
# Module identification
name: MyNewModule
version: 1.0.0
description: "My custom robotics module"

# Module configuration
module:
  tasks:
    - name: MyNewTask
      description: "Processes sensor data"
      update_rate_hz: 10  # Called 10 times per second

      # Define I/O ports (auto-generates C++ code)
      inputs:
        - name: sensor_data
          topic: "/sensors/lidar"
          message_type: "roboticks.messages.sensors.PointCloud"
          qos: "BEST_EFFORT"

      outputs:
        - name: status
          topic: "/my/status"
          message_type: "roboticks.messages.common.StringMessage"
          qos: "RELIABLE"

      # Custom configuration values
      custom:
        threshold: 0.5
        max_points: 1000

  logging:
    level: "INFO"
    log_to_file: true
    log_file: "/var/roboticks/logs/modules/mynew.log"

Step 3: Create CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(MyNewModule VERSION 1.0.0 LANGUAGES CXX)

# Find SDK root and include helpers
get_filename_component(ROBOTICKS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
include(${ROBOTICKS_ROOT}/packages/roboticks-module/cmake/RoboticksModuleHelpers.cmake)

# Auto-detects config, generates I/O code, verifies files
roboticks_module_init()

# Create the module executable
roboticks_add_module_executable(
    TARGET ${PROJECT_NAME}
    SOURCES src/MyNewTask.cpp
)

Step 4: Create Task Header

Create src/MyNewTask.hpp:
#pragma once
#include "MyNewTask.generated.hpp"  // Auto-generated base class!

class MyNewTask : public roboticks::task::MyNewTaskIO {
public:
    explicit MyNewTask(const std::string& config);
    ~MyNewTask() override = default;

protected:
    // Lifecycle hooks
    bool onInitialize() override;
    bool onStart() override;
    void onUpdate() override;
    void onStop() override;

private:
    double threshold_;
    int max_points_;
    int update_count_ = 0;
};

Step 5: Create Task Implementation

Create src/MyNewTask.cpp:
#include "MyNewTask.hpp"
#include <roboticks/module/TaskFactory.hpp>

// Auto-register with the task factory
static roboticks::module::TaskRegistrar<MyNewTask> registrar("MyNewTask");

MyNewTask::MyNewTask(const std::string& config)
    : MyNewTaskIO(config)  // Base class parses config
{
    // Read custom config values
    threshold_ = getConfigDouble("custom.threshold", 0.5);
    max_points_ = getConfigInt("custom.max_points", 1000);

    ROBOTICKS_INFO(*getLogger(),
        "MyNewTask created with threshold={}, max_points={}",
        threshold_, max_points_);
}

bool MyNewTask::onInitialize() {
    ROBOTICKS_INFO(*getLogger(), "Initializing MyNewTask...");
    // Setup resources here
    return true;
}

bool MyNewTask::onStart() {
    ROBOTICKS_INFO(*getLogger(), "Starting MyNewTask...");
    return true;
}

void MyNewTask::onUpdate() {
    update_count_++;

    // Read from input port (auto-generated member: sensor_data_input_)
    if (sensor_data_input_->hasData()) {
        auto& data = sensor_data_input_->getData();
        ROBOTICKS_DEBUG(*getLogger(), "Received {} points", data.points.size());
    }

    // Publish to output port (auto-generated member: status_output_)
    roboticks::messages::common::StringMessage msg(
        "Processing OK",
        update_count_
    );
    status_output_->publish(msg);
}

void MyNewTask::onStop() {
    ROBOTICKS_INFO(*getLogger(), "Stopped after {} updates", update_count_);
}

Step 6: Build and Run

cd build
cmake ..  # Generates MyNewTask.generated.hpp
make -j$(nproc)

# Run the module
./MyNewModule

What Gets Auto-Generated

When CMake runs, it automatically:
  1. Finds config - Locates config/MyNew.yaml
  2. Verifies source files - Checks src/MyNewTask.hpp and .cpp exist
  3. Generates I/O code - Creates build/generated/MyNewTask.generated.hpp:
// Auto-generated - DO NOT EDIT
namespace roboticks::task {

class MyNewTaskIO : public TaskBase {
protected:
    // Input ports (from YAML inputs)
    std::unique_ptr<Subscriber<PointCloud>> sensor_data_input_;

    // Output ports (from YAML outputs)
    std::unique_ptr<Publisher<StringMessage>> status_output_;

    void setupPorts() override {
        sensor_data_input_ = createSubscriber<PointCloud>(
            "/sensors/lidar", QoS::BEST_EFFORT);
        status_output_ = createPublisher<StringMessage>(
            "/my/status", QoS::RELIABLE);
    }
};

} // namespace

Naming Conventions

ComponentConventionExample
Module directoryPascalCaseMyNewModule/
Config file{Name}.yamlMyNew.yaml
Task class{Name}TaskMyNewTask
Task files{Name}Task.{hpp,cpp}MyNewTask.cpp
Port members{name}_{input,output}_sensor_data_input_

Configuration Options

Task Configuration

module:
  tasks:
    - name: TaskName
      description: "Task description"
      update_rate_hz: 10        # Update frequency
      inputs: []                # Input ports
      outputs: []               # Output ports
      custom:                   # Custom key-value config
        key: value

Port Configuration

inputs:
  - name: sensor_data          # Becomes sensor_data_input_
    topic: "/topic/name"       # Topic name
    message_type: "..."        # Message class
    qos: "BEST_EFFORT"         # RELIABLE or BEST_EFFORT

outputs:
  - name: status               # Becomes status_output_
    topic: "/status/topic"
    message_type: "..."
    qos: "RELIABLE"

Logging Configuration

logging:
  level: "DEBUG"              # DEBUG, INFO, WARN, ERROR
  log_to_file: true
  log_file: "/path/to/log.log"

Best Practices

Each module should do one thing well. Split complex functionality into multiple modules that communicate via messaging.
Topic names should describe the data: /sensors/lidar/points, /control/velocity, /status/health
  • Use RELIABLE for critical data (commands, status)
  • Use BEST_EFFORT for high-frequency sensor data
Return false from onInitialize() if setup fails - the framework will handle cleanup.

Next Steps

Messaging System

Learn about pub/sub messaging and QoS