Skip to content

Run holosoma as a service#124

Merged
tomasz-lewicki merged 74 commits into
mainfrom
dev/tomasz/tracker_service
Jul 1, 2026
Merged

Run holosoma as a service#124
tomasz-lewicki merged 74 commits into
mainfrom
dev/tomasz/tracker_service

Conversation

@tomasz-lewicki

@tomasz-lewicki tomasz-lewicki commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary / tl;dr

This PR enables running holosoma as a service that takes an external input to the whole body tracker.

  1. Added holosoma_inference_service ros2 package (desc below)
  2. Added APIs for high-level unitree clients (arm client and loco client specifically). sdk/unitree_high_level.
  3. Added a generic multiprocess_proxy.py to deal with ros2 and unitree's cyclonedds overriding each other's symbols when loaded into the same process.

Holosoma Inference Service

Service layer for deploying holosoma policies. The input/output API is ROS2 messages; a backend turns them into robot motion. The service is not required to run holosoma_inference, but can be helpful for integrating it into a larger system.

Service API

Inputs (one of):

Topic Type Description Exp. Rate
/holosoma/smplh_command CmdSMPLH.msg SMPL-H 24-joint pose targets, retargeted to dense before the policy 50Hz
/holosoma/dense_tracking_command CmdDense.msg Dense per-joint 29-DoF target (q/dq + root quat) consumed directly by the policy 50Hz
/holosoma/exoskeleton_command CmdExoskeleton.msg Left/right arm joint targets (7+7) plus base twist for the split-body controller 50Hz
/holosoma/3pt_command Cmd3pt.msg Head + wrist poses (not supported yet) 50Hz

Outputs:

Topic Type Description Rate
/holosoma/holosoma_executed_cmd JointState.msg Executed joint command, always full 29-DoF Policy Rate (typ. 50Hz)
/holosoma/heartbeat Heartbeat.msg Liveness + status 5 Hz

Note: for /holosoma/holosoma_executed_cmd, the policy backend fills all 29 values. The split-body backend fills the 14 arm joints (indices 15–28) and zeros the rest.

Internal structure

CmdSMPLH ─▶ retargeter ─┐
                        ├─CmdDense─▶ holosoma_node (WBT) ─▶ G1   (holosoma policy)
external publisher ─────┘

CmdExoskeleton ──────────────────▶ unitree_split_controller ─▶ G1   (arm_sdk + loco)

Input Support across Modes

mode \ input CmdSMPLH (24-joint) CmdDense (29-DOF) CmdExoskeleton (arm q + twist) Cmd3pt
policy (teleop_with_holosoma_policy) input_type:=smplh (retargeter) input_type:=dense (direct)
split-body (teleop_with_unitree_split_body)

Pluggable retargeter

The CmdSMPLH → CmdDense retargeter is resolved from the holosoma.retargeter entry-point group (same pattern as holosoma.sdk / holosoma.config.*), so extensions register their own embodiment-specific impl without a code change here. RetargeterNode depends on the Retargeter Protocol (retarget() + reset()); the built-in g1-smpl → G1SmplRetargeter is registered in holosoma_service/setup.py. Select at launch via retargeter:=<name> (default g1-smpl).

Build & source

Build & source before launching; re-run after editing a .msg.

cd src/holosoma_inference/holosoma_inference_service
rm -rf build install log # for a clean build
colcon build && source install/setup.bash

Run (with onnx policy)

Run with Whole-body WBT ONNX policy + SMPL-H teleop (the default)

ros2 launch holosoma_service teleop_with_holosoma_policy.launch.py \
    urdf_path:= \
    input_type:=simplh \
    model_path:=

Whole body policy with CmdDense input (retargeter off):

ros2 launch holosoma_service teleop_with_holosoma_policy.launch.py \
    input_type:=dense \
    model_path:=

Run (unitree split-body backend)

# arm_sdk + LocoClient. Robot must be standing in FSM-501.
ros2 run holosoma_service unitree_split_controller --iface eth0
ros2 run holosoma_service unitree_split_controller --iface eth0 --no-arms   # loco only

A backend does nothing without an input publisher. For the policy backend, the simplest one is the bundled NPZ replay script, which streams a reference-motion NPZ onto CmdDense as a live feed (pair with input_type:=dense):

python holosoma_service/scripts/publish_from_npz.py  --loop

Other publishers: your tracker / AVP / Pico, or ros2 run holosoma_service wasd_controller_node for mocking CmdExoskeleton.msg.

@tomasz-lewicki tomasz-lewicki changed the title Tracker Service Run holosoma as a service Jun 16, 2026
tomasz-lewicki and others added 5 commits June 24, 2026 09:34
Introduces a transport-agnostic sensor injection path alongside the
existing vel/state injection. Policies depend only on the ABC; the ROS2
implementation is a service-layer detail.

- `holosoma_inference/sensors/base.py` — `Sensor` ABC (`start`,
  `get_latest`), mirroring the `inputs/api` pattern
- `holosoma_service/policy_control/sensors.py` — `Ros2DepthSensor`:
  subscribes `sensor_msgs/Image` (32FC1, BEST_EFFORT, newest-wins),
  validates encoding, returns `(1,1,H,W)` float32 or None on timeout
- `service_node.py` — `ServiceIONode` builds a `sensors: dict[str,Sensor]`
  dict (gated on `task.depth_image_topic`) and injects it as
  `policy._injected_sensors` before `__init__`, matching the existing
  `_injected_velocity_input` pattern
- `task.py` — `depth_image_topic: str | None = None` config field
- `docs/policy_service_fsm.png` — FSM diagram of all service node state
  transitions (STIFF_HOLD → GET_READY → POLICY_RUNNING ↔ STOPPED, loco
  and dual-mode sub-states, kill/kp orthogonal commands)

Launch with depth:
  ros2 run holosoma_service policy_service_node ... \
    --task.depth-image-topic /zed/depth/image_raw
…merConfig

Move the depth preprocessing (resize -> clip -> normalize to [-0.5, 0.5]) into
Ros2DepthConsumer so the publisher sends raw metric depth (32FC1, meters) and
the consumer produces the policy-ready (N, 1, H, W) stack — matching the
on-robot image_server's _resize_clip_expand_transpose the policy trained on.

Consolidate the loose depth_* TaskConfig fields into a nested
Ros2DepthConsumerConfig (task.depth), mirroring the DebugConfig pattern.
Defaults (27x48, clip [0.1, 2.0]) match image_server; empty topics disables it.

Launch with both cameras:
  --task.depth.topics /zed_front/depth /zed_back/depth

@asetapen asetapen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍

kingb and others added 6 commits June 26, 2026 18:27
…le depth delay) (#135)

Stacks on #124. Adds a configurable depth latency to `Ros2DepthConsumer`
so a policy can be fed depth frames delayed to match the latency it was
trained against.

## Why
A ROS2 depth path is effectively instantaneous (sub-1ms), but a policy
may have trained with an inherent sensor capture/serve latency baked
into its observations. Feeding it fresher-than-trained frames is a
train/inference mismatch. This re-introduces that latency on the ROS2
side, in the **consumer**, expressed in **absolute milliseconds**
(robust across publish rate, unlike a frame count).

## What
- **`task.py`** — `Ros2DepthConsumerConfig.frame_delay_ms: float = 0.0`.
Default `0.0` preserves current freshest-frame behavior, so existing
consumers are unaffected.
- **`sensors.py`** — `Ros2DepthConsumer` buffers timestamped frame sets
in a ring buffer; `get_latest()` returns the freshest set **at least
`frame_delay_ms` old** (`None` until enough history accrues to honor the
delay). The stream-liveness `timeout` is measured against the *newest*
set, so an intentional delay is never mistaken for a dead publisher.
- **`service_node.py`** — threads `frame_delay_ms` off `task.depth` into
the consumer constructor (same path the other depth config fields take).
- **`test_depth_consumer_delay.py`** — unit tests for the look-back
selection (stubbed monotonic clock, no ROS traffic; `importorskip` for
host envs).

## Notes
The field is plain config with default `0.0`; a per-policy preset can
pin it to whatever delay that checkpoint trained with, with no change to
core. Base is `dev/tomasz/tracker_service`, so merge order is #124 then
this.
Follow-up to #135: the Static Checks job flagged two ruff issues in the
new test that the merge didn't block on.

- `pytest.raises(ValueError)` -> added `match=` (PT011)
- collapsed the multiline `Ros2DepthConsumer(...)` call (ruff-format)

Test-only; no behavior change.
Addresses review nit on PR #124: method had no callers.
@tomasz-lewicki

Copy link
Copy Markdown
Contributor Author

FYI @Kedaro — moved docker/x86/Dockerfile to src/holosoma_inference/docker/service/Dockerfile.x86 in 9ffb7e4.

Matches shoulder gains on dev/orkedar/tracker_service without editing the shared const.
Control-mode messages should not carry gripper commands.
Decouple RetargeterNode from the embodiment: it now depends on an
embodiment-agnostic Retargeter Protocol, with G1SmplRetargeter as the
G1-specific implementation. Addresses review comment on PR #124.
Discover retargeter impls from the 'holosoma.retargeter' entry-point group
(mirrors holosoma.sdk / holosoma.config.*), so extensions register their own
embodiment retargeter and select it at launch via retargeter:=<name>, no code
change in holosoma. Built-in g1-smpl -> G1SmplRetargeter registered in setup.py.
test_freejoint_strip.py lives in the retargetting package, so pytest imports
__init__.py at collection; its top-level smpl_retargeter import pulled in mink
(absent in the inference test env), erroring collection before the test's own
importorskip. Move the Retargeter import under TYPE_CHECKING.
@tomasz-lewicki tomasz-lewicki merged commit b8e0129 into main Jul 1, 2026
12 checks passed
tomasz-lewicki added a commit that referenced this pull request Jul 2, 2026
Adds a ROS2-native contract test for the service merged in #124. 

The PR tests the service's public surface ROS2 topic API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants