diff --git a/docs/usage/modules.md b/docs/usage/modules.md index 897fc1b3ec..076a4ccc76 100644 --- a/docs/usage/modules.md +++ b/docs/usage/modules.md @@ -50,9 +50,11 @@ print(CameraModule.io()) └┬─────────────┘ ├─ color_image: Image ├─ camera_info: CameraInfo + ├─ tf: TFMessage │ ├─ RPC build() -> None ├─ RPC get_skills() -> list + ├─ RPC peek_stream(stream_name: str, timeout: float) -> Any ├─ RPC set_module_ref(name: str, module_ref: RPCClient) -> None ├─ RPC set_transport(stream_name: str, transport: Transport) -> bool ├─ RPC start() -> None @@ -139,7 +141,7 @@ from dimos.hardware.sensors.camera.module import CameraModule camera = CameraModule() detector = Detection2DModule() -detector.image.connect(camera.color_image) +detector.color_image.connect(camera.color_image) camera.start() detector.start() @@ -472,3 +474,127 @@ to_svg(agentic, "assets/go2_agentic.svg") ![output](assets/go2_agentic.svg) To see more information on how to use Blueprints, see [Blueprints](/docs/usage/blueprints.md). + +## Low level manual plumbing + +Most of the time this is a bad idea, but you can peak behind blueprints, autoconnect etc, and deal with pubsub directly at lower and lower layers of abstraction + +### Connecting via transports + +`.connect` wires two modules living in the same process. Alternatively, you can assign a **transport** a typed pub/sub channel such as LCM or Zenoh. The modules never hold a reference to each other, so they can run as separate scripts. + +`camera_script.py`: + +```python skip +import time +from dimos.core.transport import LCMTransport +from dimos.hardware.sensors.camera.module import CameraModule +from dimos.msgs.sensor_msgs.Image import Image + +camera = CameraModule() +camera.color_image.transport = LCMTransport("/camera/rgb", Image) + +camera.start() +time.sleep(10) +camera.stop() +``` + +`detector_script.py`: + +```python skip +import time +from dimos.core.transport import LCMTransport +from dimos.msgs.sensor_msgs.Image import Image +from dimos.perception.detection.module2D import Detection2DModule + +detector = Detection2DModule() +detector.color_image.transport = LCMTransport("/camera/rgb", Image) + +detector.start() +detector.detections.subscribe(print) +time.sleep(10) +detector.stop() +``` + +Run each in its own terminal and detections start printing as soon as both are up. The channel name and message type must match on both sides `LCMTransport("/camera/rgb", Image)` maps to the typed channel `/camera/rgb#sensor_msgs.Image`, which you can watch with `dimos spy` or `dimos topic echo /camera/rgb`. + +Available transports live in `dimos.core.transport` (`LCMTransport`, `ZenohTransport`, `SHMTransport`, ...); see [Transports](/docs/usage/transports/index.md) for choosing between them. + +### Raw transports (no modules) + +A transport works on its own — init one and send/receive from a plain script, no module or stream declarations needed: + +```python skip +from dimos.core.transport import LCMTransport +from dimos.msgs.std_msgs.String import String + +chat = LCMTransport("/chat", String) + +unsubscribe = chat.subscribe(print) # receive +chat.publish(String(data="hello")) # send + +msg = chat.get_next() # or block for the next message +chat.stop() +``` + +Any other script (or module stream) on the same channel sees the traffic. This is handy for quick probes and debug scripts. For a robot system, prefer modules so the streams show up in blueprints and introspection. + +### Dynamic streams + +Our system is very flexible, but this is dangerous as you lose essentially all support above pubsub. Autoconnect, blueprints, configuration support (dimos --transport=...) etc + +Streams don't have to be class annotations, a module can grow inputs and outputs at runtime, and they can even be attached externally. +Construct the stream and assign it as an attribute. + +module has `inputs`/`outputs`/`io()` attributes and is a ble to discover streams attached. + +```python ansi=false +from dimos.core.module import Module +from dimos.core.stream import In, Out +from dimos.core.transport import LCMTransport +from dimos.msgs.std_msgs.String import String + +class Dyn(Module): + def start(self): + # module can add a random input + self.echo = In(String, "echo", m) + print("Externally attached output:", self.words) + +m = Dyn() + +m.words = Out(String, "words", m) +m.start() + + +m.words.transport = LCMTransport("/words", String) +m.echo.transport = LCMTransport("/words", String) + + +print("\nInputs:") +print(m.inputs) +print(m.inputs['echo']) +print("\nOutputs:") +print(m.outputs) +print(m.outputs['words']) + +print("\Send/Receive Test:") + +# we can subscribe to topics from anywhere also +m.echo.subscribe(print) +m.words.publish(String(data="loopback over LCM")) + +``` + +```results +Externally attached output: Out words[String] @ Dyn + +Inputs: +{'echo': } +In echo[String] @ Dyn via LCMTransport(/words#std_msgs.String) + +Outputs: +{'words': } +Out words[String] @ Dyn via LCMTransport(/words#std_msgs.String) +\Send/Receive Test: + +```