Package Standards
Directory Layout#
Every package follows this canonical layout, adapted to its build type:
CMake package (C++ or mixed C++/Python):
<package_name>/
├── config/ # ROS parameter files (.yaml)
├── doc/ # Documentation assets (images, diagrams)
├── include/<package_name>/ # C++ public headers
├── launch/ # Launch files (.launch.py)
├── src/ # C++ source files
├── test/ # Unit and integration tests
├── CMakeLists.txt
├── package.xml
└── README.md
Python ament package:
<package_name>/
├── config/ # ROS parameter files (.yaml)
├── doc/ # Documentation assets (images, diagrams)
├── launch/ # Launch files (.launch.py)
├── <package_name>/ # Python source package
│ └── __init__.py
├── resource/<package_name> # ament package marker file (required)
├── test/ # Unit and integration tests
├── setup.cfg
├── setup.py
├── package.xml
└── README.md
Not every directory is required in every package. A pure C++ library will have no launch/, a bringup-only package will have no src/. Omit directories that have no content. Do not create empty placeholder directories.
config/#
Parameter files live under config/ and are organized into subdirectories by concern when a package owns configuration for multiple subsystems/submodules:
config/
├── actuation/
│ └── controllers.yaml
├── localization/
│ └── ekf.yaml
└── slam/
└── mapper_params_online_async.yaml
Single-concern packages use a flat config/ with no subdirectories.
launch/#
When a package owns multiple independent launch concerns, sub-launches live in subdirectories under launch/:
launch/
├── actuation/
│ └── actuation.launch.py
├── localization/
│ └── localization.launch.py
└── bringup.launch.py
Single-concern packages use a flat launch/ with no subdirectories.
include/ and src/#
C++ public headers live under include/<package_name>/, the extra nesting level is required so that consumers include as #include "<package_name>/foo.h", which makes the origin of the header unambiguous across the workspace.
Internal implementation details go in src/.
include/micipsa_core/
├── robot_base/
│ ├── drive_wheel.h
│ ├── imu.h
│ └── robot_base.h
└── stm_protocol/
└── command.h
src/
└── robot_base/
├── drive_wheel.cpp
├── imu.cpp
└── robot_base.cpp
test/#
Tests must not duplicate the package name in the filename unless disambiguation is genuinely necessary.
doc/#
The doc/ directory contains visual and supplementary assets only, images, diagrams, GIFs.
Each type of document should be grouped into dedicated subdirectories
README.md#
Every package must have a README.md at its root. The README is the primary written documentation for the package and it should follow other packages README strcuture.
package.xml#
Dependencies are declared with the correct tag for their role:
| Tag | When to use |
|---|---|
<depend> |
Needed at both build time and runtime |
<build_depend> |
Needed only at build time |
<exec_depend> |
Needed only at runtime |
<test_depend> |
Needed only for tests |
Do not use <depend> as a catch-all. Keeping dependency types precise reduces the installed footprint of each package and makes the dependency graph easier to audit, it is also crucial because it helps create smaller Docker images
Launch File#
File Structure#
Every package-level launch file follows the same two-function pattern:
def setup_launch(context, *args, **kwargs):
# Resolve arguments, load config, build and return nodes
...
def generate_launch_description():
# Declare arguments, call OpaqueFunction(function=setup_launch)
...
generate_launch_description() only declares arguments and registers the OpaqueFunction. All node construction happens inside setup_launch(), which receives a resolved context. This is necessary because config path resolution and conditional logic depend on argument values that are only available at launch time, not at description construction time.
Launch Arguments#
The following arguments are declared consistently across all package launch files. When a package does not need an argument, it is omitted, arguments are never declared as unused placeholders.
| Argument | Type | Default | Description |
|---|---|---|---|
deploy_mode |
bool |
false |
true when running on the real robot, false for simulation |
use_sim_time |
bool |
true |
Use /clock from the simulator instead of wall time |
log_level |
string |
info |
ROS 2 log level: debug, info, warn, error |
<subsystem>_config_file |
string |
<default>.yaml |
Config file name or path |
use_sim_time defaults to true across the workspace. This is intentional, simulation is the primary development environment, and deploy mode is opt-in.
Default values are always defined at the lowest level in the launch file that directly uses the argument, not in any upstream caller. In a chain like bringup.launch.py → driver.launch.py → camera.launch.py, the default for a camera argument lives in camera.launch.py. Upstream files forward arguments they receive but never re-declare defaults for arguments they do not own. This ensures there is exactly one place where a default can be wrong, and that place is always as close as possible to the code that acts on it.
Config Resolution#
Package launch files do not hardcode config paths. Config loading follows a consistent two-step resolution pattern via resolve_config_path() from micipsa_common.launch_utils:
1. Look in micipsa_bringup/config/<subsystem>/ for the given filename
2. Fall back to <package>/config/ if not found in bringup
3. Warn and use the package default if neither is found
config_path = resolve_config_path(
config_file,
bringup_pkg_share,
package_pkg_share,
bringup_config_subdir="config/localization",
calling_config_subdir="config",
)
if config_path is None:
if package_pkg_share:
print(
warn(
f"Given Config file not found, using default config: '{DEFAULT_CONFIG_FILE_NAME}'"
)
)
config_path = os.path.join(
package_pkg_share, "config", DEFAULT_CONFIG_FILE_NAME
)
else:
print(
warn(
f"Cannot fall back to default config: '{PACKAGE_NAME}' package not found, skipping package launch"
)
)
return []
micipsa_bringup is the system-level configuration authority. Packages look there first so that system-tuned parameters take precedence over package defaults without requiring the package launch file to be modified. The fallback to the package's own config/ ensures the package remains runnable without micipsa_bringup present.
The config file argument accepts a filename (e.g. ekf.yaml) or an absolute path (e.g. /home/myname/ros2_ws/src/micipsa/micipsa_robot/micipsa_localization/config/ekf.yaml).

