====================
== Gyungmin Myung ==
====================
Born to do fun stuff!

Robotics Needed Shared Conventions, Not a Parallel Universe of Software Engineering

Robotics

The problem was not unusual. I needed to send small state and command messages between C++, Rust, and Python processes at around 500 Hz. I wanted clear interfaces, reasonable latency, and a system that could be built and deployed without much ceremony.

That should have been a normal software-engineering problem.

Instead, it immediately turned into a discussion about DDS implementations, QoS profiles, executors, callback groups, ROS distributions, Ubuntu versions, message packages, ament, colcon, rosdep, workspace overlays, setup scripts, and Docker images.

None of those things is individually absurd. Distributed systems are difficult. Native dependencies are difficult. Robots have requirements that ordinary backend services do not.

What bothered me was the scope of the decision.

I was supposedly choosing how two processes should exchange data. Somehow, I was also choosing the package format, dependency resolver, build workflow, concurrency model, deployment environment, and operating-system release for most of the robot.

That is the larger problem I have with ROS.

ROS gave robotics a badly needed set of shared conventions. Over time, those conventions became attached to a separate ecosystem for doing almost every other part of software engineering.

Robotics needed shared domain conventions. It did not need a parallel universe of software engineering.

What ROS got right

It is easy to criticize ROS now because we forget why it succeeded. Without common conventions, every robotics group invents its own image format, joint-state representation, coordinate system, robot description, recorder, and visualizer. Reusing another group’s planner can mean reconstructing half of its software stack.

ROS gave robotics a common vocabulary.

A camera driver and a vision algorithm could agree on an image representation. TF standardized relationships between coordinate frames. URDF made robot descriptions reusable. Rosbag made experiments recordable and replayable. RViz made robots inspectable without another custom tool.

Those ideas are valuable. What I want to discard is the assumption that adopting them should require the rest of ROS.

The useful part is agreement about meaning: units, coordinate frames, joint names, timestamps, covariance, and required fields. None of that inherently requires a ROS package, colcon, ROS logging, ROS parameters, or a ROS executor.

Somewhere along the way, the implementation became inseparable from the convention.

ROS mixes three different kinds of problem

I find it useful to divide ROS functionality into three groups.

The first is genuinely robotics-specific:

  • coordinate frames;
  • robot descriptions;
  • sensor and actuator schemas;
  • joint conventions;
  • simulation time;
  • trajectories;
  • recording and replay of multimodal data;
  • visualization of robots and their surroundings.

The second consists of distributed-systems problems:

  • serialization;
  • publish-subscribe communication;
  • RPC;
  • discovery;
  • reliability;
  • queueing;
  • backpressure;
  • observability.

The third is ordinary software infrastructure:

  • package management;
  • dependency resolution;
  • build systems;
  • threads and timers;
  • logging;
  • configuration;
  • service supervision;
  • deployment;
  • release management.

ROS is strongest in the first group. In the second, it is one possible set of engineering choices. DDS has useful properties, especially for dynamic publish-subscribe graphs and configurable delivery semantics, but it is not the only reasonable way to connect robot software. The third group is where ROS loses me.

I do not see why agreeing on the meaning of an odometry message should commit a project to a particular build metaframework, package manifest, workspace layout, callback runtime, and operating-system lifecycle.

The robotics-specific parts of ROS are its greatest contribution. The ROS-specific versions of ordinary software infrastructure are increasingly its biggest liability.

You can, of course, keep algorithms in ordinary libraries, isolate ROS in thin adapters, and use only the parts you need. That is the architecture I am arguing for.

But it is not the path of least resistance. ROS messages lead to ROS packages, packages lead to workspaces, and tutorials commonly put timing, parameters, logging, and application state inside nodes. Clean boundaries require deliberate resistance to the default.

We no longer lack alternatives

Building outside ROS once meant writing nearly everything yourself. That is much less true today.

Elsewhere in software, multi-language systems forced teams to define stable interfaces between independently built and deployed components. The result is a mature selection of async runtimes, serialization formats, RPC systems, queues, tracing tools, and build systems.

They were not designed for robots, and hard real-time control has its own requirements. Most of the surrounding problems, however, are not unique to robotics.

For almost every infrastructure role ROS fills, modern alternatives already exist. They are not all drop-in replacements, and no individual tool replaces ROS as a whole. They can be selected independently.

A rough mapping looks like this:

ROS roleExisting options
.msg, .srv, and .action definitionsProtobuf, FlatBuffers, Cap’n Proto, OMG IDL, JSON Schema
ROS services and actionsgRPC, Connect RPC, Zenoh queries, ordinary async APIs
ROS topics and discoveryZenoh, DDS directly, NATS, MQTT, QUIC, application-specific UDP
RosbagMCAP
RViz and data inspectionFoxglove, Rerun, custom WebGPU or Three.js tools
ROS logging and diagnosticsOpenTelemetry, structured logging, Prometheus
ROS launchPython or shell scripts, lightweight process runners, containers when needed
ROS parametersTyped configuration files, environment variables, RPC configuration APIs
package.xml and rosdepNix, Pixi, uv, native package managers
colcon and amentBazel, Buck2, Meson, CMake, Cargo
URDF and simulation descriptionsSDFormat, MJCF, OpenUSD-based descriptions
Executors, timers, and callbacksStandard threads, async runtimes, explicit event loops

Not every option is suitable for every robot. MQTT is not automatically a sensible control transport. gRPC over TCP does not behave like best-effort DDS over a lossy radio. A custom Three.js visualizer may take far more work than launching RViz.

ROS is no longer the only viable option at most of these layers. The missing piece is agreement.

The separation is already happening

The strongest reason to keep ROS everywhere is interoperability. If one team publishes sensor_msgs/Image, another team and its tools already know what it means. The value comes from agreement on the schema and its semantics, not from the package containing its generated code.

Foxglove has demonstrated this with framework-agnostic schemas available for Protobuf, JSON Schema, FlatBuffers, ROS 1, ROS 2, and OMG IDL. The same model can travel over a live connection or be stored in MCAP without making the producer a ROS application.

MCAP makes a similar separation at the recording layer. It stores schemas with timestamped messages while remaining independent of the live transport. It is the default rosbag2 storage format beginning with ROS 2 Iron, but software outside ROS can consume it.

These projects do not replace the ROS interface ecosystem. Foxglove’s schemas focus on data representation and visualization. MCAP does not define coordinate conventions, clock policy, or actuator services. Company-maintained schemas are also not neutral community standards.

Still, the separation is no longer hypothetical.

The robotics community could maintain versioned, transport-independent definitions for common data and hardware services. A standard should define units, coordinate-frame rules, timestamp semantics, covariance layouts, compatibility rules, and valid ranges.

It should not dictate the transport, package manager, executor, build environment, or process launcher.

We can agree on a transform schema without agreeing on ROS, a point-cloud schema without agreeing on DDS, and an MCAP channel convention without agreeing on the live transport. The community needs a small set of boring, well-governed standards, not another all-encompassing framework.

A transport dependency slowly takes over the program

ROS usually enters a project at the boundary.

At first, it is the transport between independent programs. Then interfaces become ROS messages, message definitions require packages, implementations acquire package.xml files, and colcon gathers everything into a workspace.

Soon application code contains rclcpp::Node, rclcpp::Time, publishers, subscriptions, timers, parameters, and loggers. ROS is no longer carrying data at the boundary. It owns the program’s time, configuration, lifecycle, scheduling, and public types.

Removing ROS later is therefore rarely as simple as replacing DDS. The difficult part is extracting the application from a framework that has spread through its control flow and data model.

For my original 500 Hz problem, I wanted controllers and estimators to remain ordinary libraries. A transport adapter could supply state from DDS, gRPC, shared memory, a simulator, or a test fixture without changing the application itself.

Instead, much of the ROS software I encounter looks like this:

ROS node
 ├── transport
 ├── configuration
 ├── timing
 ├── callbacks
 ├── logging
 ├── state
 └── actual algorithm

The algorithm is not a library used by a ROS adapter.

It is a tenant inside the middleware.

Dependencies become archaeology

When I clone a ROS project, the instructions usually say to run rosdep, install a list of apt packages, or both. Sometimes they omit dependencies that happened to be present on the maintainer’s machine.

Rosdep maps dependency declarations onto operating-system packages. In practice, requirements are split across package.xml, rosdep’s mapping database, CMakeLists.txt, the README, apt commands, and the developer’s existing system.

Installation becomes archaeology: run rosdep, encounter an unresolved key, find an old apt command in an issue, install it manually, and try again. Eventually the workspace compiles, but no one can clearly describe the resulting environment.

Modern package tools are not perfect, but uv and Pixi at least produce a resolved description of the environment. Nix reduces accidental reliance on globally installed files.

ROS has dependency metadata.

What it often lacks is one clear and complete answer to the question:

What exactly must exist for this repository to build?

When native installation is too painful, the answer is often “use our Docker image.” Containers are useful for deployment, isolation, and CI. They are not a replacement for a coherent dependency model.

A Docker image preserves a working filesystem; it does not explain why it works. Libraries, ROS packages, setup scripts, manual binaries, and undocumented fixes may simply be frozen inside it.

Robotics also weakens the container boundary. Programs need cameras, CAN devices, GPUs, shared memory, host networking, graphical output, real-time privileges, and vendor kernel drivers. After enough devices, capabilities, volumes, and sockets have been forwarded, the container is another userspace awkwardly bolted onto the host.

A container should be an output of a reproducible build and dependency process, not the only location where the project is known to work.

Six years later, projects still require Ubuntu 20.04

The operating-system coupling makes this worse.

As I write this in July 2026, I still regularly find robotics repositories that require Ubuntu 20.04.

Ubuntu 20.04 was released in April 2020, and its standard support ended on May 31, 2025.

There are legitimate reasons to maintain an old operating system. Industrial deployments value stability, hardware certification takes time, and a robot in the field should not be upgraded casually.

That is not usually what I see in research repositories. Instead of a controlled product lifecycle, the README says only that Ubuntu 20.04 was tested and other versions may not work.

When a messaging and integration framework determines the operating system of the entire robot, it is no longer merely middleware.

A build metaframework around other build systems

The build story follows the same pattern.

ROS 2 uses colcon to orchestrate packages. C++ usually goes through ament_cmake; Python through ament_python. package.xml stores ROS metadata, while rosdep maps external dependencies onto operating-system packages.

Each part has a reason to exist. Together, they leave a mixed-language repository juggling package.xml, CMake, Python packaging, ament macros, rosdep keys, colcon discovery, workspace overlays, and generated environment hooks. This is not one multilingual build system. It is a convention for placing several build systems inside a ROS workspace.

That becomes obvious when a project contains something outside the expected C++ and Python paths: Rust, firmware, frontend assets, WebAssembly, or cross-compilation.

It can usually be made to work, but it feels like persuading the workspace to tolerate another software ecosystem. General build tools are not simple either; Bazel has plenty of complexity. But it explicitly models builds across languages and target platforms and tracks build inputs rather than relying on a sourced workspace.

ROS did not create a true multilingual build system.

It created a build metaframework organized around ROS packages.

ROS also wants to own control flow

ROS provides clocks, timers, executors, callback groups, parameters, lifecycle states, and shutdown behavior. Some solve real problems: simulation time matters, and integrated timers and subscriptions are convenient.

The problem is that these abstractions become the default even when ordinary concurrency tools would be clearer. Callbacks hide scheduling decisions until latency, deadlocks, ordering, or overload matter. Developers must then reconstruct which callbacks share a thread, what blocked what, and how much work is queued.

I have seen developers become fluent in nodes, topics, and callback groups before learning threads, channels, mutexes, and bounded queues. That is not an indictment of them. It is a predictable result of presenting general concurrency problems through framework-specific abstractions.

When those abstractions leak, configuring an executor is no substitute for understanding synchronization, ownership, backpressure, and scheduling.

A motor-command path may need a one-element latest-value queue; a recorder, a larger bounded queue. The component that owns the work should make those decisions directly.

The real job is standardization, not another framework

A bag of independent tools does not create an ecosystem. Protobuf does not define what an IMU measurement means. gRPC does not define coordinate-frame semantics. MCAP does not choose channel names or units.

The community still needs canonical schemas, clock and frame rules, common hardware services, versioning policies, transport adapters, and conformance tests. They do not all need to belong to one runtime.

Then a C++ driver could expose the same standard interface through Zenoh, gRPC, shared memory, or ROS. A Python analysis tool could read the same recording. A simulator could implement the same service contract without joining the production runtime. Interoperability would come from standards and conformance, not from requiring every program to inhabit the same framework.

That is how robotics software should mature: not by creating “ROS 3,” but by pulling the genuinely useful conventions out of the framework and making them portable.

What leaving ROS costs

There is a reason ROS became dominant.

Leaving it means giving up convenience.

ROS has a large driver ecosystem. RViz is useful. Nav2 and MoveIt contain years of work. Hardware vendors commonly publish ROS support first. Researchers can exchange components quickly because the conventions and runtime are already understood.

A decomposed system requires more architecture work up front. Someone has to define the interfaces and choose the transport. Bridges need maintenance. Recording and visualization need deliberate solutions. There is no single package that replaces everything ROS provides.

For a university prototype, that cost may not be worth paying.

For a product expected to survive several hardware generations, language migrations, and operating-system upgrades, it may be.

ROS optimizes for getting unrelated components connected quickly. A decomposed architecture can optimize for owning those components over time. Those are different goals.

Keep the conventions

I am moving away from ROS because choosing how components communicate should not determine almost every other engineering decision.

Robotics still needs standard schemas, coordinate conventions, robot descriptions, simulation interfaces, recording formats, visualization tools, and reusable algorithms.

There are already modern tools for essentially every infrastructure function around those conventions.

What we lack is not another framework.

What we lack is community agreement on portable standards.

A transform convention should not dictate a package manager.

A joint-state schema should not dictate a build system.

A visualizer should not dictate the live transport.

A recording format should not dictate the runtime.

A timer should not require a node and an executor unless it actually needs those semantics.

A driver should be usable as a normal library.

A robot should be a software system, not a workspace.

Robotics needed shared domain conventions. It did not need a parallel universe of software engineering.

Keep the conventions.

Standardize them properly.

Then let robotics software rejoin the rest of software engineering.