Run holosoma as a service#124
Merged
Merged
Conversation
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
Co-authored-by: Ori Kedar <orkedar@amazon.com>
… dev/tomasz/tracker_service
…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.
… from torchvision
Addresses review nit on PR #124: method had no callers.
Contributor
Author
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary / tl;dr
This PR enables running holosoma as a service that takes an external input to the whole body tracker.
holosoma_inference_serviceros2 package (desc below)sdk/unitree_high_level.Holosoma Inference Service
Service layer for deploying
holosomapolicies. The input/output API is ROS2 messages; a backend turns them into robot motion. The service is not required to runholosoma_inference, but can be helpful for integrating it into a larger system.Service API
Inputs (one of):
/holosoma/smplh_commandCmdSMPLH.msg/holosoma/dense_tracking_commandCmdDense.msg/holosoma/exoskeleton_commandCmdExoskeleton.msg/holosoma/3pt_commandCmd3pt.msgOutputs:
/holosoma/holosoma_executed_cmdJointState.msg/holosoma/heartbeatHeartbeat.msgNote: 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
CmdSMPLH(24-joint)CmdDense(29-DOF)CmdExoskeleton(arm q + twist)Cmd3ptteleop_with_holosoma_policy)input_type:=smplh(retargeter)input_type:=dense(direct)teleop_with_unitree_split_body)Pluggable retargeter
The
CmdSMPLH → CmdDenseretargeter is resolved from theholosoma.retargeterentry-point group (same pattern asholosoma.sdk/holosoma.config.*), so extensions register their own embodiment-specific impl without a code change here.RetargeterNodedepends on theRetargeterProtocol (retarget()+reset()); the built-ing1-smpl → G1SmplRetargeteris registered inholosoma_service/setup.py. Select at launch viaretargeter:=<name>(defaultg1-smpl).Build & source
Build & source before launching; re-run after editing a
.msg.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
CmdDenseinput (retargeter off):ros2 launch holosoma_service teleop_with_holosoma_policy.launch.py \ input_type:=dense \ model_path:=Run (unitree split-body backend)
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
CmdDenseas a live feed (pair withinput_type:=dense):Other publishers: your tracker / AVP / Pico, or
ros2 run holosoma_service wasd_controller_nodefor mockingCmdExoskeleton.msg.