mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-26 03:06:01 +00:00
feat(unitree_g1): episode reset, lazy replay decode, safe shutdown
- reset(): pause the background controller and, for full-body controllers, publish the default pose directly (new _controller_paused flag) so reset and the controller loop aren't both writing low commands. - SONIC pipeline: add reset() to StandingEncoderDecoder and PlannerController (clear token/proprio history/heading, rewind motion buffer); SonicRuntime.reset() now calls controller.reset(). - sonic_whole_body: require the full dense 34-D command (no silent zero-fill of a partial action) and integrate yaw-rate (idx 33) into heading. - controllers/__init__: import the controller classes referenced in __all__. - unitree_g1: lazy replay-frame decode + small cache instead of decoding all frames up front; safer disconnect (longer controller-thread join + fail-safe that skips the graceful ramp if the thread won't stop). - lint: ruff-format config_unitree_g1 hand_closed_pose; prettier README table.
This commit is contained in:
@@ -80,9 +80,7 @@ class UnitreeG1Config(RobotConfig):
|
|||||||
publish_hands: bool = False
|
publish_hands: bool = False
|
||||||
hand_open_grip_value: float = 1.0
|
hand_open_grip_value: float = 1.0
|
||||||
hand_closed_grip_value: float = 0.0
|
hand_closed_grip_value: float = 0.0
|
||||||
hand_closed_pose: list[float] = field(
|
hand_closed_pose: list[float] = field(default_factory=lambda: [1.0, 0.9, 0.9, 1.3, 1.3, 1.3, 1.3])
|
||||||
default_factory=lambda: [1.0, 0.9, 0.9, 1.3, 1.3, 1.3, 1.3]
|
|
||||||
)
|
|
||||||
hand_kp: float = 1.5
|
hand_kp: float = 1.5
|
||||||
hand_kd: float = 0.1
|
hand_kd: float = 0.1
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,13 @@
|
|||||||
|
|
||||||
"""Unitree G1 locomotion controllers (Groot, Holosoma, SONIC)."""
|
"""Unitree G1 locomotion controllers (Groot, Holosoma, SONIC)."""
|
||||||
|
|
||||||
|
from .gr00t_locomotion import GrootLocomotionController
|
||||||
|
from .holosoma_locomotion import HolosomaLocomotionController
|
||||||
|
from .sonic_whole_body import SonicRuntime, SonicWholeBodyController
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GrootLocomotionController",
|
"GrootLocomotionController",
|
||||||
"HolosomaLocomotionController",
|
"HolosomaLocomotionController",
|
||||||
"SonicWholeBodyController",
|
|
||||||
"SonicRuntime",
|
"SonicRuntime",
|
||||||
|
"SonicWholeBodyController",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -128,7 +128,15 @@ DEBUG_PRINT_EVERY = 100 # ticks between debug prints
|
|||||||
|
|
||||||
|
|
||||||
def _to_mujoco(a):
|
def _to_mujoco(a):
|
||||||
"""Reorder a 29-vector from IsaacLab order into MuJoCo/deploy order."""
|
"""Apply the ``MUJOCO_TO_ISAACLAB`` gather to a 29-vector (deploy-order reorder).
|
||||||
|
|
||||||
|
NOTE: this returns ``a[MUJOCO_TO_ISAACLAB]``. The ``_mj`` suffixes and the exact
|
||||||
|
permutation direction throughout this module are a fixed convention validated
|
||||||
|
against the deployed SONIC ONNX policy (the encoder/decoder consume vectors in
|
||||||
|
this order). Do not "correct" the table or rename toward the opposite direction
|
||||||
|
without re-validating on hardware — the labels are historical, the ordering is
|
||||||
|
load-bearing.
|
||||||
|
"""
|
||||||
return a[MUJOCO_TO_ISAACLAB]
|
return a[MUJOCO_TO_ISAACLAB]
|
||||||
|
|
||||||
|
|
||||||
@@ -318,6 +326,23 @@ class StandingEncoderDecoder:
|
|||||||
self.smpl_root_quat = None
|
self.smpl_root_quat = None
|
||||||
self.set_zero_reference()
|
self.set_zero_reference()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Clear the token, 10-frame proprioception history and heading init.
|
||||||
|
|
||||||
|
``UnitreeG1.reset()`` relies on this so the first decoder outputs of a new
|
||||||
|
episode are not contaminated by the previous episode's state.
|
||||||
|
"""
|
||||||
|
self.token = np.zeros(TOKEN_DIM, np.float32)
|
||||||
|
self.last_action_mj = np.zeros(29, np.float32)
|
||||||
|
self.h_q_mj = [np.zeros(29, np.float32)] * 10
|
||||||
|
self.h_dq_mj = [np.zeros(29, np.float32)] * 10
|
||||||
|
self.h_ang = [np.zeros(3, np.float32)] * 10
|
||||||
|
self.h_act_mj = [np.zeros(29, np.float32)] * 10
|
||||||
|
self.h_quat = [np.array([1, 0, 0, 0], np.float32)] * 10
|
||||||
|
self.init_base_quat = np.array([1, 0, 0, 0], np.float32)
|
||||||
|
self.init_ref_quat = np.array([1, 0, 0, 0], np.float32)
|
||||||
|
self._heading_init = False
|
||||||
|
|
||||||
def update_history(self, q, dq, ang, quat):
|
def update_history(self, q, dq, ang, quat):
|
||||||
"""Push the latest proprioception (pos/vel/gyro/orientation) into the 10-frame buffers."""
|
"""Push the latest proprioception (pos/vel/gyro/orientation) into the 10-frame buffers."""
|
||||||
quat = quat / (np.linalg.norm(quat) + 1e-8)
|
quat = quat / (np.linalg.norm(quat) + 1e-8)
|
||||||
@@ -499,6 +524,28 @@ class PlannerController(StandingEncoderDecoder):
|
|||||||
self.playing = self.first_motion = False
|
self.playing = self.first_motion = False
|
||||||
self.motion_lock = threading.Lock()
|
self.motion_lock = threading.Lock()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Full reset: clear enc/dec state (super) plus the motion buffer and heading.
|
||||||
|
|
||||||
|
Forces a heading re-init on the next ``step`` so the reference frame is
|
||||||
|
re-latched to the post-reset robot orientation.
|
||||||
|
"""
|
||||||
|
super().reset()
|
||||||
|
with self.motion_lock:
|
||||||
|
self.ref_cursor = 0
|
||||||
|
self.motion_timesteps = 0
|
||||||
|
self.motion_joint_positions[:] = 0.0
|
||||||
|
self.motion_joint_velocities[:] = 0.0
|
||||||
|
self.motion_body_quats[:] = 0.0
|
||||||
|
self.motion_body_quats[:, 0] = 1.0
|
||||||
|
self.motion_body_pos[:] = 0.0
|
||||||
|
self.init_ref_quat = np.array([1, 0, 0, 0], np.float64)
|
||||||
|
self.heading_init_base_quat = np.array([1, 0, 0, 0], np.float64)
|
||||||
|
self.delta_heading = 0.0
|
||||||
|
self.first_motion = False
|
||||||
|
self.playing = False
|
||||||
|
self.reinit_heading = True
|
||||||
|
|
||||||
def _heading_apply_delta(self):
|
def _heading_apply_delta(self):
|
||||||
"""Heading correction quaternion (init base-vs-ref heading + operator ``delta_heading``)."""
|
"""Heading correction quaternion (init base-vs-ref heading + operator ``delta_heading``)."""
|
||||||
delta = quat_mul(
|
delta = quat_mul(
|
||||||
|
|||||||
@@ -65,10 +65,15 @@ def _extract_wb34_from_action(action: dict | None) -> np.ndarray | None:
|
|||||||
units. The ``.pos`` suffix lets these flow through ``lerobot-rollout`` as normal
|
units. The ``.pos`` suffix lets these flow through ``lerobot-rollout`` as normal
|
||||||
joint-position action features.
|
joint-position action features.
|
||||||
"""
|
"""
|
||||||
if not action or wb_action_key(0) not in action:
|
if not action:
|
||||||
|
return None
|
||||||
|
keys = [wb_action_key(i) for i in range(WB_ACTION_DIM)]
|
||||||
|
# Require the full dense command: a partial action (e.g. only ``wb.0.pos``)
|
||||||
|
# must not be silently zero-filled, which would drive most joints toward 0.
|
||||||
|
if any(key not in action for key in keys):
|
||||||
return None
|
return None
|
||||||
return np.fromiter(
|
return np.fromiter(
|
||||||
(float(action.get(wb_action_key(i), 0.0)) for i in range(WB_ACTION_DIM)),
|
(float(action[key]) for key in keys),
|
||||||
dtype=np.float32,
|
dtype=np.float32,
|
||||||
count=WB_ACTION_DIM,
|
count=WB_ACTION_DIM,
|
||||||
)
|
)
|
||||||
@@ -86,8 +91,9 @@ def _wb34_to_reference(wb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|||||||
The 29 joints are first assembled in MuJoCo / Unitree-SDK order
|
The 29 joints are first assembled in MuJoCo / Unitree-SDK order
|
||||||
([L-leg 0:6, R-leg 6:12, waist 12:15, L-arm 15:22, R-arm 22:29] — the
|
([L-leg 0:6, R-leg 6:12, waist 12:15, L-arm 15:22, R-arm 22:29] — the
|
||||||
``G1_29_JointIndex`` grouping OpenHLM uses), then permuted to IsaacLab order via
|
``G1_29_JointIndex`` grouping OpenHLM uses), then permuted to IsaacLab order via
|
||||||
``MUJOCO_TO_ISAACLAB``. Grippers (7, 15) and yaw-rate (33) are not part of the
|
``MUJOCO_TO_ISAACLAB``. Grippers (7, 15) are not part of the 29-DoF SONIC
|
||||||
29-DoF SONIC reference.
|
reference, and yaw-rate (33) is integrated into the heading by the caller (it
|
||||||
|
cannot be represented in this static per-tick anchor).
|
||||||
"""
|
"""
|
||||||
ref_mj = np.zeros(29, np.float32) # MuJoCo / Unitree-SDK grouped order
|
ref_mj = np.zeros(29, np.float32) # MuJoCo / Unitree-SDK grouped order
|
||||||
ref_mj[0:6] = wb[16:22] # left leg
|
ref_mj[0:6] = wb[16:22] # left leg
|
||||||
@@ -129,8 +135,10 @@ class SonicRuntime:
|
|||||||
return self.controller
|
return self.controller
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.controller.reinit_heading = True
|
# Full pipeline reset: clears the encoder token, proprioception history and
|
||||||
self.controller.playing = True
|
# heading, and rewinds the motion buffer. reinit_heading is set so the next
|
||||||
|
# step re-latches the reference frame to the current robot orientation.
|
||||||
|
self.controller.reset()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
pass
|
pass
|
||||||
@@ -165,6 +173,9 @@ class SonicWholeBodyController:
|
|||||||
# stream of per-tick whole-body commands, fed to the encoder as a batch.
|
# stream of per-tick whole-body commands, fed to the encoder as a batch.
|
||||||
self._wb_traj: deque[np.ndarray] = deque(maxlen=50)
|
self._wb_traj: deque[np.ndarray] = deque(maxlen=50)
|
||||||
self._wb_quat_traj: deque[np.ndarray] = deque(maxlen=50)
|
self._wb_quat_traj: deque[np.ndarray] = deque(maxlen=50)
|
||||||
|
# Integrated heading (rad) from the whole-body command's yaw-rate (index 33),
|
||||||
|
# forwarded to the pipeline as ``delta_heading`` so turn commands take effect.
|
||||||
|
self._heading = 0.0
|
||||||
|
|
||||||
logger.info("SONIC ready (encoder/decoder, 34-D whole-body command path)")
|
logger.info("SONIC ready (encoder/decoder, 34-D whole-body command path)")
|
||||||
|
|
||||||
@@ -181,6 +192,11 @@ class SonicWholeBodyController:
|
|||||||
if c.encode_mode != 0:
|
if c.encode_mode != 0:
|
||||||
c.encode_mode = 0
|
c.encode_mode = 0
|
||||||
c.reinit_heading = True
|
c.reinit_heading = True
|
||||||
|
# Index 33 is a yaw-rate (rad/s): integrate it into a heading offset and hand
|
||||||
|
# it to the pipeline as ``delta_heading`` so commanded turns are tracked rather
|
||||||
|
# than silently dropped (the anchor from _wb34_to_reference only carries r/p).
|
||||||
|
self._heading += float(wb[33]) * CONTROL_DT
|
||||||
|
c.delta_heading = self._heading
|
||||||
# Capture the heading/anchor reference on the first whole-body tick. The
|
# Capture the heading/anchor reference on the first whole-body tick. The
|
||||||
# controller only latches ``init_ref_quat`` (and the base heading) inside
|
# controller only latches ``init_ref_quat`` (and the base heading) inside
|
||||||
# ``step()`` when ``first_motion or reinit_heading`` — but it already boots in
|
# ``step()`` when ``first_motion or reinit_heading`` — but it already boots in
|
||||||
@@ -282,6 +298,7 @@ class SonicWholeBodyController:
|
|||||||
self._wb_step = 0
|
self._wb_step = 0
|
||||||
self._wb_traj.clear()
|
self._wb_traj.clear()
|
||||||
self._wb_quat_traj.clear()
|
self._wb_quat_traj.clear()
|
||||||
|
self._heading = 0.0
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self._runtime.shutdown()
|
self._runtime.shutdown()
|
||||||
|
|||||||
@@ -161,41 +161,62 @@ class UnitreeG1(Robot):
|
|||||||
|
|
||||||
# Controller thread state
|
# Controller thread state
|
||||||
self._controller_thread = None
|
self._controller_thread = None
|
||||||
|
# When set, the controller loop stops publishing low commands so reset() can
|
||||||
|
# drive the joints directly without two publishers fighting (single-publisher).
|
||||||
|
self._controller_paused = threading.Event()
|
||||||
self._controller_action_lock = threading.Lock()
|
self._controller_action_lock = threading.Lock()
|
||||||
self.controller_input = default_remote_input()
|
self.controller_input = default_remote_input()
|
||||||
self.controller_output = {}
|
self.controller_output = {}
|
||||||
|
|
||||||
# Replay-camera state (decoded frames per robot camera name + play cursor).
|
# Replay-camera state: keep the encoded (raw) cells per camera and decode
|
||||||
self._replay_frames: dict[str, list[np.ndarray]] = {}
|
# frames lazily as the play cursor advances, with a small frame cache, so we
|
||||||
|
# don't materialize gigabytes of decoded RGB at construction time.
|
||||||
|
self._replay_raw: dict[str, list] = {}
|
||||||
|
self._replay_cache: dict[tuple[str, int], np.ndarray] = {}
|
||||||
|
self._replay_cache_cap = 8
|
||||||
self._replay_len = 0
|
self._replay_len = 0
|
||||||
self._replay_idx = 0
|
self._replay_idx = 0
|
||||||
if config.replay_camera_parquet and config.replay_camera_map:
|
if config.replay_camera_parquet and config.replay_camera_map:
|
||||||
self._load_replay_frames()
|
self._load_replay_frames()
|
||||||
|
|
||||||
def _load_replay_frames(self) -> None:
|
def _load_replay_frames(self) -> None:
|
||||||
"""Decode recorded episode frames from a parquet into per-camera image lists."""
|
"""Load only the mapped parquet columns (encoded frames); decode on demand."""
|
||||||
import io
|
|
||||||
|
|
||||||
import pyarrow.parquet as pq
|
import pyarrow.parquet as pq
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
table = pq.read_table(self.config.replay_camera_parquet)
|
cols_needed = list(dict.fromkeys(self.config.replay_camera_map.values()))
|
||||||
cols = {col: table.column(col).to_pylist() for col in self.config.replay_camera_map.values()}
|
table = pq.read_table(self.config.replay_camera_parquet, columns=cols_needed)
|
||||||
self._replay_len = table.num_rows
|
self._replay_len = table.num_rows
|
||||||
|
self._replay_raw = {
|
||||||
def decode(cell) -> np.ndarray:
|
cam_name: table.column(column).to_pylist()
|
||||||
data = cell["bytes"] if isinstance(cell, dict) else cell
|
for cam_name, column in self.config.replay_camera_map.items()
|
||||||
return np.asarray(Image.open(io.BytesIO(data)).convert("RGB"), dtype=np.uint8)
|
}
|
||||||
|
|
||||||
for cam_name, column in self.config.replay_camera_map.items():
|
|
||||||
self._replay_frames[cam_name] = [decode(c) for c in cols[column]]
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Loaded %d replay frames for cameras %s from %s",
|
"Loaded %d replay frames (lazy-decode) for cameras %s from %s",
|
||||||
self._replay_len,
|
self._replay_len,
|
||||||
list(self.config.replay_camera_map),
|
list(self.config.replay_camera_map),
|
||||||
self.config.replay_camera_parquet,
|
self.config.replay_camera_parquet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _decode_replay_cell(self, cell) -> np.ndarray:
|
||||||
|
import io
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
data = cell["bytes"] if isinstance(cell, dict) else cell
|
||||||
|
return np.asarray(Image.open(io.BytesIO(data)).convert("RGB"), dtype=np.uint8)
|
||||||
|
|
||||||
|
def _replay_frame(self, cam_name: str, idx: int) -> np.ndarray:
|
||||||
|
"""Decode (and briefly cache) a single replay frame for a camera."""
|
||||||
|
key = (cam_name, idx)
|
||||||
|
cached = self._replay_cache.get(key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
frame = self._decode_replay_cell(self._replay_raw[cam_name][idx])
|
||||||
|
if len(self._replay_cache) >= self._replay_cache_cap:
|
||||||
|
self._replay_cache.pop(next(iter(self._replay_cache)))
|
||||||
|
self._replay_cache[key] = frame
|
||||||
|
return frame
|
||||||
|
|
||||||
def _subscribe_lowstate(self): # polls robot state @ 250Hz
|
def _subscribe_lowstate(self): # polls robot state @ 250Hz
|
||||||
while not self._shutdown_event.is_set():
|
while not self._shutdown_event.is_set():
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -292,8 +313,10 @@ class UnitreeG1(Robot):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _replay_cameras_ft(self) -> dict[str, tuple]:
|
def _replay_cameras_ft(self) -> dict[str, tuple]:
|
||||||
"""Replay cameras, shaped from their first decoded frame."""
|
"""Replay cameras, shaped from their first (lazily decoded) frame."""
|
||||||
return {name: frames[0].shape for name, frames in self._replay_frames.items() if frames}
|
if not self._replay_len:
|
||||||
|
return {}
|
||||||
|
return {name: self._replay_frame(name, 0).shape for name in self._replay_raw}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def observation_features(self) -> dict[str, type | tuple]:
|
def observation_features(self) -> dict[str, type | tuple]:
|
||||||
@@ -333,6 +356,11 @@ class UnitreeG1(Robot):
|
|||||||
while not self._shutdown_event.is_set():
|
while not self._shutdown_event.is_set():
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Paused during reset() so the reset routine is the sole low-cmd publisher.
|
||||||
|
if self._controller_paused.is_set():
|
||||||
|
time.sleep(control_dt)
|
||||||
|
continue
|
||||||
|
|
||||||
with self._lowstate_lock:
|
with self._lowstate_lock:
|
||||||
lowstate = self._lowstate
|
lowstate = self._lowstate
|
||||||
|
|
||||||
@@ -495,14 +523,23 @@ class UnitreeG1(Robot):
|
|||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
# Stop the controller loop first so it isn't fighting the shutdown ramp.
|
# Stop the controller loop first so it isn't fighting the shutdown ramp.
|
||||||
self._shutdown_event.set()
|
self._shutdown_event.set()
|
||||||
|
controller_stopped = True
|
||||||
if self._controller_thread is not None:
|
if self._controller_thread is not None:
|
||||||
self._controller_thread.join(timeout=2.0)
|
# Wait long enough for any in-flight inference tick to finish and the loop
|
||||||
|
# to observe the shutdown flag, so no stray low command is published while
|
||||||
|
# the ramp runs (the shutdown routine must be the single publisher).
|
||||||
|
self._controller_thread.join(timeout=5.0)
|
||||||
if self._controller_thread.is_alive():
|
if self._controller_thread.is_alive():
|
||||||
logger.warning("Controller thread did not stop cleanly")
|
controller_stopped = False
|
||||||
|
logger.error(
|
||||||
|
"Controller thread did not stop; skipping graceful ramp to avoid "
|
||||||
|
"concurrent low commands (fail-safe: joints keep last command until exit)"
|
||||||
|
)
|
||||||
|
|
||||||
# Soft, damped settle instead of an instant limp (real robot only; the
|
# Soft, damped settle instead of an instant limp (real robot only; the
|
||||||
# subscribe thread is still alive here to supply the current pose).
|
# subscribe thread is still alive here to supply the current pose). Only ramp
|
||||||
if not self.config.is_simulation:
|
# once the controller thread has definitely exited.
|
||||||
|
if not self.config.is_simulation and controller_stopped:
|
||||||
self._graceful_stop()
|
self._graceful_stop()
|
||||||
|
|
||||||
if self.controller is not None and hasattr(self.controller, "shutdown"):
|
if self.controller is not None and hasattr(self.controller, "shutdown"):
|
||||||
@@ -569,8 +606,8 @@ class UnitreeG1(Robot):
|
|||||||
idx = self._replay_idx
|
idx = self._replay_idx
|
||||||
if idx >= self._replay_len:
|
if idx >= self._replay_len:
|
||||||
idx = self._replay_len - 1 if not self.config.replay_camera_loop else idx % self._replay_len
|
idx = self._replay_len - 1 if not self.config.replay_camera_loop else idx % self._replay_len
|
||||||
for name, frames in self._replay_frames.items():
|
for name in self._replay_raw:
|
||||||
obs[name] = frames[idx]
|
obs[name] = self._replay_frame(name, idx)
|
||||||
self._replay_idx += 1
|
self._replay_idx += 1
|
||||||
|
|
||||||
# Cameras - read images from ZMQ cameras
|
# Cameras - read images from ZMQ cameras
|
||||||
@@ -709,6 +746,18 @@ class UnitreeG1(Robot):
|
|||||||
if default_positions is None:
|
if default_positions is None:
|
||||||
default_positions = np.array(self.config.default_positions, dtype=np.float32)
|
default_positions = np.array(self.config.default_positions, dtype=np.float32)
|
||||||
|
|
||||||
|
# Full-body controllers (SONIC / OpenHLM) own the whole 29-DoF command and
|
||||||
|
# ignore ``<joint>.q`` in send_action(), so reset() must publish the default
|
||||||
|
# pose directly. Pause the background controller first so the two aren't both
|
||||||
|
# writing low commands while the robot moves to the default pose.
|
||||||
|
full_body = getattr(self.controller, "full_body", False)
|
||||||
|
paused = False
|
||||||
|
if full_body and self._controller_thread is not None:
|
||||||
|
self._controller_paused.set()
|
||||||
|
paused = True
|
||||||
|
time.sleep(control_dt) # let any in-flight controller tick settle
|
||||||
|
|
||||||
|
try:
|
||||||
if self.config.is_simulation and self.sim_env is not None:
|
if self.config.is_simulation and self.sim_env is not None:
|
||||||
self.sim_env.reset()
|
self.sim_env.reset()
|
||||||
self.publish_lowcmd(
|
self.publish_lowcmd(
|
||||||
@@ -737,6 +786,11 @@ class UnitreeG1(Robot):
|
|||||||
interp_pos = init_dof_pos[motor.value] * (1 - alpha) + target_pos * alpha
|
interp_pos = init_dof_pos[motor.value] * (1 - alpha) + target_pos * alpha
|
||||||
action_dict[f"{motor.name}.q"] = float(interp_pos)
|
action_dict[f"{motor.name}.q"] = float(interp_pos)
|
||||||
|
|
||||||
|
# Full-body controllers no-op in send_action(); publish the pose
|
||||||
|
# directly (arm-only controllers keep the send_action() path).
|
||||||
|
if full_body:
|
||||||
|
self.publish_lowcmd(action_dict)
|
||||||
|
else:
|
||||||
self.send_action(action_dict)
|
self.send_action(action_dict)
|
||||||
|
|
||||||
# Maintain constant control rate
|
# Maintain constant control rate
|
||||||
@@ -744,8 +798,12 @@ class UnitreeG1(Robot):
|
|||||||
sleep_time = max(0, control_dt - elapsed)
|
sleep_time = max(0, control_dt - elapsed)
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
# Reset controller internal state (gait phase, obs history, etc.)
|
# Reset controller internal state (gait phase, obs history, etc.) before
|
||||||
|
# resuming so its buffers reflect the post-reset pose.
|
||||||
if self.controller is not None and hasattr(self.controller, "reset"):
|
if self.controller is not None and hasattr(self.controller, "reset"):
|
||||||
self.controller.reset()
|
self.controller.reset()
|
||||||
|
finally:
|
||||||
|
if paused:
|
||||||
|
self._controller_paused.clear()
|
||||||
|
|
||||||
logger.info("Reached default position")
|
logger.info("Reached default position")
|
||||||
|
|||||||
Reference in New Issue
Block a user