This is a problem I’ve run into multiple times, and I don’t think I’ve found “The Solution” (if one even exists). There are many factors related to your own codebase and development style, as well as organizational needs, robot usage patterns, etc. that can impact what solution will work well for you. With that said, I’ll try to share some ideas that I’ve found work reasonably well.
As a note, I have only approached this problem with ROS1, so not everything may apply similarly.
Both organizations that I have worked on this problem at have used debian package based deployment (as opposed to docker). I am also personally a fan of mono-repo whenever possible (I mention this because the suggestions of some others to use different repos for each configuration make me shudder a bit). In this scenario, I have found it useful to use separate ROS packages for configuration files and launch files for each robot platform/model. I like to style my launch files like this:
- Top level launch file
- include launch file
- include launch file 1
- default params for 1 (native or included)
- include launch file 2
- default params for 1 (native or included)
- …
- include launch file N
- load params 1 for
- load params 2 for
- …
- load params N for
- include launch file 1
- include launch file
It’s important that the param loading is last.
What this effectively does is let you have launch files for individual nodes that include default parameters (making the launch file useful standalone and providing documentation of the parameters), and then override those parameters in files that are unique to your specific platform that is getting loaded.
There are additional cases where you may need parameters unique to individual robots (sensor and peripheral serial numbers, or robot-specific tuning params, etc). To handle this, we use a central webserver that stores robotic-specific configuration parameters as an environment variable file. Whenever a robot comes online, it syncs the file to its system. When the software starts up, the file is sourced into the that runtime environment. The environment variables are also used to select the correct top-level launch file (basically, specifying the , above).
Using this process, we have
- Source control for all platform-specific configurations
- Centralized server for all robot-specific configurations
- This is advantageous for feature flags and other real-time param changes without a full software deploy
- This also makes it easy for others in the organization to see and make minor configurations to a robot, without needing to be git-savvy
- Hierarchical launch file structure that takes advantage of overrides
Some Challenges:
- Tracing which value is used for a particular parameter (default in code at initialization? default in code from
NodeHandle::param()
? value in launch file? value in param file? value in param override file? value on server config file?. Ideally, you could just look at the lowest-level config file and see, but you don’t always override everything. If you make a mistake with parameter naming (or namespacing), this can also be misleading. - Adding a new parameter or modifying the value of a parameter can require touching a lot of files
- Guaranteeing that a parameter was found
- To combat this, we have started to use a helper function that wraps standard ROS param loading that throws an exception if a parameter is not found. It’s not perfect, but it has helped us catch things quickly in simulation and automated tests.