Skip to content

Extend With Your Components

estherRay edited this page Aug 17, 2026 · 1 revision

This page covers three ways to extend LOTUSim: adding a new vessel model, adding a new sensor type, and adding a new power provider/consumer type.

Contents

Just want to use an existing sensor or battery on your model, not build a new one? See Customising a model in the Tutorial instead. This page is for adding brand-new component types to the codebase.


Adding models into LOTUSim

GUI (recommended method)

Go to the model page and click the + button.

Manual method

  1. Under assets/models, create a new folder named after your model.
  2. Create a .sdf file inside assets/models/[your-model-name]/ in SDF format. This is where you declare which sensors and which battery/generator your model uses, see Customising a model for how to add them.
  3. Add your model's .stl file to the same folder.
  4. If you want dynamics to work with your model, create a .yaml file defining the forces acting on it and its dynamics matrices.

Adding sensors

LOTUSim provides a sensor plugin framework so you can plug in your own sensor type. A new sensor is a self-contained ROS 2/ament package, following the same structure as the built-in sensors (e.g. ais_sensor).

1. Create your sensor package

Under systems/sensors, create a new folder for your sensor with the standard layout:

systems/sensors/your_sensor/
├── CMakeLists.txt
├── package.xml
├── include/your_sensor/your_sensor.hpp
└── src/your_sensor.cpp

2. Subclass CustomSensor

Your sensor class (in lotusim_sensor_base) must implement two virtual methods:

class YourSensor : public CustomSensor {
public:
    YourSensor(
        std::shared_ptr<spdlog::logger> logger,
        rclcpp::Node::SharedPtr node,
        const gz::sim::Entity& vessel_entity,
        const gz::sim::Entity& sensor_entity,
        const std::string& parent_name,
        const std::string& sensor_name);
 
    virtual bool UpdateSensor(
        const gz::sim::UpdateInfo& _info,
        const gz::sim::EntityComponentManager& _ecm) final;
 
private:
    virtual bool CustomSensorLoad(const sdf::Sensor& _sdf) final;
};
  • CustomSensorLoad() reads your sensor's SDF parameters (e.g. update rate, noise) at load time
  • UpdateSensor() runs every tick and publishes your sensor's ROS 2 message

3. Set up your package's build files

  • CMakeLists.txt: build your_sensor as a shared library depending on lotusim_sensor_base and lotusim_common (mirror ais_sensor/CMakeLists.txt)
  • package.xml: declare the same dependencies

4. Register your sensor with the plugin

Add your_sensor as a dependency of lotusim_sensor_plugin:

  • In lotusim_sensor_plugin/package.xml, add <depend>your_sensor</depend>
  • In lotusim_sensor_plugin/CMakeLists.txt, add find_package(your_sensor REQUIRED) and include it in ament_target_dependencies Then, in lotusim_sensor_plugin.cpp, add a branch for your sensor's type inside LotusimSensorPlugin::EachNew:
else if (type == "your_sensor_type") {
    m_logger->info(
        "LotusimSensorPlugin::YourSensor: Creating sensor [{}/{}]",
        model_name,
        sensor_name);
    sensor = CreateSensor<YourSensor>(
        data,
        model_entity,
        _entity,
        model_name,
        sensor_name);
}

type comes from your sensor's gz:type attribute in the model's SDF (e.g. gz:type="your_sensor_type").

5. Rebuild

lotusim install

Power management: adding new provider/consumer types

The power subsystem needs at least one power provider enabled on your vessel to activate and one power consumer to actually draw down the battery - see Customising a model to enable an existing one.

Adding a new provider type

  1. Create a subclass of Battery or Generator
  2. Implement receiveLoad(), voltage(), and any other overrides
  3. Add a new else if (type == "your_type") branch in PlatformPowerManagerBase::initPowerProvider()
  4. Add the new .cpp to CMakeLists.txt

Adding a new consumer type

  1. Create a subclass of PowerConsumer
  2. Implement drawnCurrent(), receiveVoltage(), isActive(), deactivate()
  3. Add a new else if (powerType == "your_type") branch in PlatformPowerManagerBase::initPowerConsumers()
  4. Add the <lotusim_power> block to the relevant SDF element

📖 Full architecture details are in the Power Subsystem README.


Your World File and Your Plugin

World File

In Gazebo, the world defines the environment parameters and which models are present in the simulation. Worlds are defined in <world_name>.world files, see the assets/worlds folder for all available examples.

The default world we recommend to use is lotusim.world.

Components of a world file

Element Purpose
<physics> Time management: step size, real-time factor, update rate
<plugin> Enables systems for the world, e.g. custom sensors, waypoint following, power management
<include> Adds a model to the world: remember to set <pose> to position it correctly
<experimental:params> Overrides parameters on an included model (see below)

Physics and core plugins, from lotusim.world:

<physics type="ode">
    <max_step_size>0.2</max_step_size>
    <real_time_factor>1</real_time_factor>
    <real_time_update_rate>1</real_time_update_rate>
</physics>
 
<plugin filename="physics_interface_plugin" name="lotusim::gazebo::PhysicsInterfacePlugin"/>
<plugin filename="multi_agent_system_plugin" name="lotusim::gazebo::MultiAgentSystem"/>
<plugin filename="lotusim_sensor_plugin" name="lotusim::sensor::LotusimSensorPlugin"/>
<plugin filename="power_subsystem" name="lotusim::gazebo::PowerManager"/>

Adding a model with <include>, from circling_ship_example.world:

<include>
    <uri>model://dtmb_hull</uri>
    <name>fremm</name>
    <pose>0 0 0 0 0 0</pose>
 
    <lotus_param>
        <waypoint_follower>
            <follower>
                <loop>true</loop>
                <circle>
                    <radius>20</radius>
                </circle>
            </follower>
        </waypoint_follower>
    </lotus_param>
</include>

Your Plugin

Custom plugins (systems), like waypoint_plugin or power_subsystem, are separate packages under systems/, built as shared libraries and registered with Gazebo through the <plugin> element shown above.

  1. Create your plugin package under systems/your_plugin/, with the standard CMakeLists.txt / package.xml / include/ / src/ layout (mirror waypoint_follower).

  2. Implement your plugin class, inheriting from gz::sim::System plus whichever interfaces you need (e.g. ISystemConfigure, ISystemPreUpdate, ISystemUpdate, ISystemPostUpdate):

namespace your_namespace {
 
class YourPlugin
    : public gz::sim::System,
      public gz::sim::ISystemConfigure,
      public gz::sim::ISystemPostUpdate {
public:
    void Configure(
        const gz::sim::Entity& _entity,
        const std::shared_ptr<const sdf::Element>& _sdf,
        gz::sim::EntityComponentManager& _ecm,
        gz::sim::EventManager& _eventMgr) override;
 
    void PostUpdate(
        const gz::sim::UpdateInfo& _info,
        const gz::sim::EntityComponentManager& _ecm) override;
};
 
}  // namespace your_namespace
 
GZ_ADD_PLUGIN(
    your_namespace::YourPlugin,
    gz::sim::System,
    your_namespace::YourPlugin::ISystemConfigure,
    your_namespace::YourPlugin::ISystemPostUpdate)
  1. Build it as a shared library in CMakeLists.txt:
add_library(your_plugin SHARED
  src/your_plugin.cpp
)
target_link_libraries(your_plugin
  gz-sim8::gz-sim8
  gz-plugin2::gz-plugin2
)
  1. Declare it in your world file.

The filename must match your CMake library target name, and name must match your fully-qualified C++ class (namespace + class name):

<plugin filename="your_plugin" name="your_namespace::YourPlugin">
</plugin>
  1. Rebuild:
lotusim install

A note on declaration order: if your plugin depends on data another plugin produces (for example, the power subsystem needs to run before sensors check whether they're powered), declare your plugin before the ones that depend on it in the world file.

Clone this wiki locally