How to Add Support for New Robot Hardware to the DimOS Module System
To add support for new robot hardware to the DimOS module system, implement a protocol-compliant adapter in dimos/hardware/<category>/<vendor>/, register it with the AdapterRegistry, wrap it in a Module subclass that exposes typed streams and RPC methods, and compose it into a blueprint listed in dimos/robot/all_blueprints.py.
DimOS (Dimensional OS) enables developers to treat any robot component—sensors, drive-trains, or manipulators—as modular units that expose typed streams (In[T], Out[T]) and RPC methods. Adding support for new robot hardware follows a structured pipeline that keeps hardware-specific code isolated from high-level logic through the adapter pattern implemented in the dimensionalOS/dimos repository.
Core Architecture of the DimOS Hardware Abstraction
Understanding the four-layer architecture is essential before implementing new hardware support.
-
ModuleBase/Module(located indimos/core/module.py): The base class for all runtime modules. It handles stream creation, RPC registration via the@rpcdecorator, and graceful shutdown. When you subclassModule, the__init_subclass__method (lines 90–110) automatically converts class attributes likejoint_positions: Out[list[float]]into concrete stream objects at runtime. -
Stream types (
In,Out,RemoteOut): Defined indimos/core/stream.py, these typed data pipes are auto-wired by the blueprint system based on name and type matching. -
Hardware specification protocols: Found in paths like
dimos/hardware/manipulators/spec.py, theseProtocolclasses (decorated with@runtime_checkable) define the uniform API that every concrete driver must implement—methods likeconnect(),disconnect(), and domain-specific calls such asread_joint_positions(). -
Adapter registry: The
AdapterRegistryclass indimos/hardware/manipulators/registry.pyprovides auto-discovery via thediscover()method, which scans subpackages foradapter.pyfiles containing aregister(registry)function. -
Blueprint composition: The
autoconnecthelper indimos/core/blueprints.pywires modules together by matching stream names and types, whiledimos/robot/all_blueprints.pyserves as the central registry making blueprints addressable via the CLI (dimos run …).
Step 1: Define or Select a Hardware Protocol
If you are adding a completely new category of hardware (not manipulators, cameras, or drive-trains), first define a protocol that enforces structural typing:
# dimos/hardware/<category>/spec.py
from typing import Protocol, runtime_checkable
from enum import Enum
class MyHardwareStatus(Enum):
DISCONNECTED = "disconnected"
CONNECTED = "connected"
ERROR = "error"
@runtime_checkable
class MyHardwareAdapter(Protocol):
"""Protocol that every concrete driver for this hardware must implement."""
def connect(self) -> bool: ...
def disconnect(self) -> None: ...
def get_status(self) -> MyHardwareStatus: ...
If you are extending an existing category (e.g., adding a new manipulator arm), reuse the existing spec such as ManipulatorAdapter in dimos/hardware/manipulators/spec.py.
Step 2: Implement the Concrete Adapter
Create a subpackage under the appropriate hardware category and implement the adapter class. The class must satisfy the protocol through structural typing—it does not need to explicitly inherit from the protocol—and must expose a register function.
# dimos/hardware/manipulators/xarm/adapter.py
from dimos.hardware.manipulators.spec import ManipulatorAdapter, ControlMode
from dimos.hardware.manipulators.registry import AdapterRegistry
class XArmAdapter:
"""Thin wrapper around the XArm SDK."""
def __init__(self, ip: str, dof: int = 6):
self.ip = ip
self.dof = dof
def connect(self) -> bool:
return True # SDK connection logic here
def disconnect(self) -> None: ...
def is_connected(self) -> bool: ...
def get_info(self): ...
def set_control_mode(self, mode: ControlMode) -> bool: ...
def read_joint_positions(self) -> list[float]: ...
def write_joint_positions(self, positions: list[float], velocity: float = 1.0) -> bool: ...
def register(registry: AdapterRegistry) -> None:
"""Called by the auto-discovery system in registry.py."""
registry.register("xarm", XArmAdapter)
Place this file at dimos/hardware/<category>/<vendor>/adapter.py. Ensure the subpackage has an __init__.py so the registry can import it.
Step 3: Verify Auto-Discovery
The AdapterRegistry automatically discovers adapters when you import the registry module. The registry is instantiated at import time in dimos/hardware/manipulators/registry.py:
adapter_registry = AdapterRegistry()
adapter_registry.discover()
After adding your adapter, verify it appears in the available adapters:
from dimos.hardware.manipulators.registry import adapter_registry
print(adapter_registry.available()) # Should include "xarm"
No manual registration in central files is required—the register function in your adapter.py handles everything.
Step 4: Create the Module Wrapper
Modules translate low-level adapter calls into typed streams and RPCs that the rest of the system can consume. Inherit from Module (which extends ModuleBase) and define streams as class attributes.
# dimos/hardware/manipulators/xarm/module.py
import asyncio
from dimos.core.module import Module
from dimos.core.stream import Out
from dimos.core.core import rpc
from dimos.hardware.manipulators.registry import adapter_registry
class XArmModule(Module):
# Streams exposed to downstream modules
joint_positions: Out[list[float]]
joint_efforts: Out[list[float]]
def __init__(self, ip: str, dof: int = 6):
super().__init__()
# Create the low-level adapter using the registry factory
self.adapter = adapter_registry.create("xarm", ip=ip, dof=dof)
@rpc
def start(self) -> None:
self.adapter.connect()
async def publish_loop():
while self.adapter.is_connected():
self.joint_positions.publish(self.adapter.read_joint_positions())
await asyncio.sleep(0.02) # 50 Hz
asyncio.create_task(publish_loop())
@rpc
def move_to(self, positions: list[float], velocity: float = 1.0) -> str:
success = self.adapter.write_joint_positions(positions, velocity)
return "OK" if success else "Failed"
The super().__init__() call wires the RPC transport and creates the asyncio event loop. Streams defined as class attributes are automatically instantiated as concrete Out objects.
Step 5: Compose the Blueprint
Blueprints tie hardware modules together and expose them to the CLI. Use the autoconnect helper to automatically wire streams.
# dimos/robot/manipulators/xarm/blueprints.py
from dimos.core.blueprints import autoconnect
from dimos.hardware.manipulators.xarm.module import XArmModule
def xarm_simple():
"""Expose a single XArm manipulator as a standalone stack."""
return autoconnect(
XArmModule(ip="192.168.1.10", dof=6),
)
Step 6: Register and Launch
Add the blueprint entry to dimos/robot/all_blueprints.py to make it discoverable by the CLI:
# dimos/robot/all_blueprints.py
"demo-xarm": "dimos.robot.manipulators.xarm.blueprints:xarm_simple",
Launch the hardware stack:
dimos run demo-xarm --log-level debug
Inspect the running streams:
dimos lcmspy # Shows joint_positions topic publishing
Call RPC methods from the agent or other modules:
dimos agent-send "move arm to [0.0, 0.5, -0.3, 0.0, 0.0, 0.0]"
Complete Integration Example
Here is a minimal, copy-pasteable example for a fictional "AwesomeArm" manipulator:
# dimos/hardware/manipulators/awesome/adapter.py
from dimos.hardware.manipulators.spec import ManipulatorAdapter, ControlMode
from dimos.hardware.manipulators.registry import AdapterRegistry
class AwesomeAdapter:
def __init__(self, serial: str, dof: int = 7):
self.serial = serial
self.dof = dof
def connect(self) -> bool:
return True
def disconnect(self) -> None: ...
def is_connected(self) -> bool: ...
def get_info(self): ...
def set_control_mode(self, mode: ControlMode) -> bool: ...
def read_joint_positions(self) -> list[float]: ...
def write_joint_positions(self, positions: list[float], velocity: float = 1.0) -> bool: ...
def register(reg: AdapterRegistry) -> None:
reg.register("awesome", AwesomeAdapter)
# dimos/hardware/manipulators/awesome/module.py
import asyncio
from dimos.core.module import Module
from dimos.core.stream import Out
from dimos.core.core import rpc
from dimos.hardware.manipulators.registry import adapter_registry
class AwesomeModule(Module):
joint_positions: Out[list[float]]
def __init__(self, serial: str):
super().__init__()
self.adapter = adapter_registry.create("awesome", serial=serial)
@rpc
def start(self) -> None:
self.adapter.connect()
async def poll():
while self.adapter.is_connected():
self.joint_positions.publish(self.adapter.read_joint_positions())
await asyncio.sleep(0.05)
asyncio.create_task(poll())
@rpc
def move_to(self, positions: list[float], vel: float = 1.0) -> str:
return "OK" if self.adapter.write_joint_positions(positions, vel) else "FAIL"
# dimos/robot/manipulators/awesome/blueprints.py
from dimos.core.blueprints import autoconnect
from dimos.hardware.manipulators.awesome.module import AwesomeModule
def awesome_arm():
return autoconnect(AwesomeModule(serial="ABC123"))
Summary
- Implement a protocol-compliant adapter in
dimos/hardware/<category>/<vendor>/adapter.pythat exposes aregister(registry)function for auto-discovery. - Create a Module subclass in
dimos/hardware/<category>/<vendor>/module.pythat instantiates the adapter viaadapter_registry.create(), defines typed streams (Out[T]), and exposes RPCs using the@rpcdecorator. - Compose a blueprint using
autoconnectindimos/robot/<category>/<vendor>/blueprints.pyto wire the module into the system. - Register the blueprint in
dimos/robot/all_blueprints.pyto enable CLI access viadimos run. - Verify integration using
dimos lcmspyto inspect streams anddimos agent-sendto test RPC calls.
Frequently Asked Questions
How does the AdapterRegistry auto-discovery mechanism work?
The AdapterRegistry in dimos/hardware/manipulators/registry.py scans all subpackages of dimos.hardware.<category> for files named adapter.py. When it finds a register(registry) function, it executes it, allowing the adapter to register itself under a string key (like "xarm" or "awesome"). This eliminates the need to manually edit central configuration files when adding new hardware vendors.
Do I need to modify core DimOS files to add new hardware support?
No. You only create new files within the dimos/hardware/ and dimos/robot/ directories. The AdapterRegistry discovers your adapter automatically, and all_blueprints.py only requires a single line addition to expose your blueprint to the CLI. You never need to modify dimos/core/module.py or the stream definitions.
What is the difference between an adapter and a module?
The adapter (XArmAdapter) is a thin wrapper around the vendor SDK that implements the hardware protocol (e.g., ManipulatorAdapter). It handles low-level connections and raw data. The module (XArmModule) is a DimOS runtime component that instantiates the adapter, converts its data into typed streams (Out[list[float]]), and exposes RPC methods (@rpc) that other modules or LLM agents can call. Modules run inside the DimOS event loop; adapters are plain Python classes.
How do I expose hardware data to perception or navigation modules?
Define typed stream attributes on your Module class, such as joint_positions: Out[list[float]] or camera_frames: Out[np.ndarray]. The Module base class converts these annotations into concrete stream objects at runtime. Downstream modules declare compatible In[T] attributes with matching type signatures. When you compose blueprints using autoconnect(), DimOS automatically wires Out streams to In streams based on type and name matching, allowing zero-configuration data flow between hardware and perception pipelines.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →