Conversation
Greptile SummaryThis PR adds initial integration support for the Booster K1 humanoid robot, introducing a Key findings:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent
participant BoosterK1SkillContainer
participant K1Connection
participant BoosterRPC as booster-rpc SDK
participant Robot as Booster K1 Robot
Agent->>BoosterK1SkillContainer: move(x, y, yaw, duration)
BoosterK1SkillContainer->>K1Connection: RPC move(twist, duration=duration)
K1Connection->>BoosterRPC: _call(ROBOT_MOVE, RobotMoveRequest)
BoosterRPC->>Robot: UDP/RPC command
Note over K1Connection: duration is never used ⚠️
K1Connection-->>BoosterK1SkillContainer: True
BoosterK1SkillContainer-->>Agent: "Started moving..."
Agent->>BoosterK1SkillContainer: standup()
BoosterK1SkillContainer->>K1Connection: RPC standup()
K1Connection->>BoosterRPC: _call(GET_ROBOT_STATUS)
BoosterRPC-->>K1Connection: status
alt mode == DAMPING
K1Connection->>BoosterRPC: _call(CHANGE_MODE, PREPARE)
Note over K1Connection: sleep(3s)
end
K1Connection->>BoosterRPC: _call(CHANGE_MODE, WALKING)
Note over K1Connection: sleep(3s)
K1Connection-->>BoosterK1SkillContainer: True
loop Background thread
K1Connection->>BoosterRPC: WebSocket connect(ws://ip:port)
BoosterRPC-->>K1Connection: JPEG frame bytes
K1Connection->>K1Connection: _on_frame(jpeg_bytes)
K1Connection->>K1Connection: color_image.publish(image)
end
Last reviewed commit: 106528b |
| def move(self, twist: Twist, duration: float = 0.0) -> bool: | ||
| """Send movement command to robot.""" | ||
| if not self._conn: | ||
| return False | ||
| try: | ||
| req = RobotMoveRequest(vx=twist.linear.x, vy=twist.linear.y, vyaw=twist.angular.z) | ||
| self._conn._call(RpcApiId.ROBOT_MOVE, bytes(req)) | ||
| return True | ||
| except Exception as e: | ||
| logger.debug("Move command failed: %s", e) | ||
| return False |
There was a problem hiding this comment.
duration parameter is accepted but silently ignored
The move method signature accepts a duration parameter and its docstring promises "How long to move (seconds)", and BoosterK1SkillContainer.move explicitly passes duration=duration through the RPC. However, the body never uses duration — it sends the motion command once and returns immediately.
This means agents that instruct the robot to e.g. "move forward for 2 seconds" will send one velocity packet and stop with no timed hold, causing unintended behaviour. Compare with G1Connection.move, which forwards duration to self.connection.move(twist, duration).
A minimal fix is to add a timed hold (if duration > 0) followed by a stop command:
@rpc
def move(self, twist: Twist, duration: float = 0.0) -> bool:
"""Send movement command to robot."""
if not self._conn:
return False
try:
req = RobotMoveRequest(vx=twist.linear.x, vy=twist.linear.y, vyaw=twist.angular.z)
self._conn._call(RpcApiId.ROBOT_MOVE, bytes(req))
if duration > 0:
time.sleep(duration)
stop = RobotMoveRequest(vx=0.0, vy=0.0, vyaw=0.0)
self._conn._call(RpcApiId.ROBOT_MOVE, bytes(stop))
return True
except Exception as e:
logger.debug("Move command failed: %s", e)
return False| def _on_frame(self, jpeg_bytes: bytes) -> None: | ||
| if not self._running: | ||
| raise KeyboardInterrupt |
There was a problem hiding this comment.
raise KeyboardInterrupt used as flow control
KeyboardInterrupt is a user-interrupt signal (Ctrl+C), not an internal stop signal. Raising it from _on_frame works here only because _stream_video has an explicit except KeyboardInterrupt: break, but it has two drawbacks:
- Any code between
self._on_frame(frame)and thatexceptblock that also catchesBaseException(or bareexcept:) could suppress it unexpectedly. - It obscures intent — a reader seeing
raise KeyboardInterruptin a normal callback will be confused.
A cleaner pattern is to simply return early (or raise a small custom exception), since the outer while self._running loop already handles the stop case:
def _on_frame(self, jpeg_bytes: bytes) -> None:
if not self._running:
return
arr = cv2.imdecode(np.frombuffer(jpeg_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
...| match global_config.viewer_backend: | ||
| case "rerun": | ||
| from dimos.visualization.rerun.bridge import rerun_bridge | ||
|
|
||
| with_vis = autoconnect(_transports_base, rerun_bridge(**rerun_config)) | ||
| case "rerun-web": | ||
| from dimos.visualization.rerun.bridge import rerun_bridge | ||
|
|
||
| with_vis = autoconnect(_transports_base, rerun_bridge(viewer_mode="web", **rerun_config)) | ||
| case _: | ||
| with_vis = autoconnect(_transports_base) | ||
|
|
||
| booster_k1_basic = autoconnect( | ||
| with_vis, | ||
| k1_connection(), | ||
| websocket_vis(), | ||
| ).global_config(n_dask_workers=4, robot_model="booster_k1") |
There was a problem hiding this comment.
Module-level match block executes at import time with no isolation
The match global_config.viewer_backend: block (and the conditional from ... import rerun_bridge calls) run at module import time. This means the blueprint value is baked in at the moment the module is first imported, regardless of when or how booster_k1_basic is actually used.
If global_config.viewer_backend is set after this module is imported (e.g. lazily via another subsystem), or if the module is imported during a test with a different backend, the wrong with_vis branch will be chosen and the result is silently incorrect.
This is the same pattern used in some existing blueprints, but it is worth flagging here as it could cause subtle bugs when unit-testing or when the viewer backend is configured late in the startup sequence.
| @@ -0,0 +1,255 @@ | |||
| #!/usr/bin/env python3 | |||
There was a problem hiding this comment.
Missing __init__.py for dimos/robot/booster/ and dimos/robot/booster/k1/
The directories dimos/robot/booster/ and dimos/robot/booster/k1/ have no __init__.py file. Every other comparable level in this codebase (e.g. dimos/robot/unitree/__init__.py) has one. Without them, Python may treat these as implicit namespace packages, which works in many cases but can fail if the project uses tools or configurations that expect regular packages (e.g. certain editable-install backends, import guards, or __init__-based lazy loaders). All blueprint __init__.py files inside k1/ are present; it is only the two parent packages that are missing.
Please add empty dimos/robot/booster/__init__.py and dimos/robot/booster/k1/__init__.py files to match the project convention.
There was a problem hiding this comment.
Skill containers are not used anymore
There was a problem hiding this comment.
I'll rebase against dev, this branch is old and I based it on the unitree-go2 blueprints
Problem
This merge request adds support for the Booster K1 robot, along with accompanying blueprints (
booster-k1-basic,booster-k1-agentic).Note
This relies on booster-rpc (GitHub source), a library I wrote that provides high-level control of the Booster K1 by interfacing with the same RPC and WebSocket protocols used by the Booster App. Through booster-rpc, Dimensional will be the first and only way to control the Booster wirelessly with no installation or SSH required. Additionally, the App and Dimensional can control the robot concurrently.
Closes DIM-XXX
Solution
Breaking Changes
None
How to Test
ROBOT_IP can be found on the Booster App

Contributor License Agreement