restore gravity_compensation

This commit is contained in:
Martino Russi
2026-07-29 18:12:19 +02:00
parent cdf5141688
commit 3ae036ee40
3 changed files with 28 additions and 23 deletions
@@ -83,6 +83,9 @@ class UnitreeG1Config(RobotConfig):
# Cameras (ZMQ-based remote cameras) # Cameras (ZMQ-based remote cameras)
cameras: dict[str, CameraConfig] = field(default_factory=dict) cameras: dict[str, CameraConfig] = field(default_factory=dict)
# Compensates for gravity on the unitree's arms using the arm ik solver
gravity_compensation: bool = False
# When False, connect() does not start the background controller thread, so a # When False, connect() does not start the background controller thread, so a
# caller can drive the controller synchronously (one decode per fed action), # caller can drive the controller synchronously (one decode per fed action),
# reproducing the deploy's single 50Hz control clock for faithful replay. # reproducing the deploy's single 50Hz control clock for faithful replay.
@@ -99,11 +99,6 @@ REMOTE_AXES = ("remote.lx", "remote.ly", "remote.rx", "remote.ry")
REMOTE_BUTTONS = tuple(f"remote.button.{i}" for i in range(16)) REMOTE_BUTTONS = tuple(f"remote.button.{i}" for i in range(16))
REMOTE_KEYS = REMOTE_AXES + REMOTE_BUTTONS REMOTE_KEYS = REMOTE_AXES + REMOTE_BUTTONS
# Reserved action-dict field used to forward the set of currently-pressed keyboard
# keys from a KeyboardTeleop through the standard action pipeline to a controller.
KEYBOARD_KEYS_FIELD = "keyboard.keys"
def default_remote_input() -> dict[str, float]: def default_remote_input() -> dict[str, float]:
"""Return a zeroed-out remote input dict (axes + buttons).""" """Return a zeroed-out remote input dict (axes + buttons)."""
return dict.fromkeys(REMOTE_KEYS, 0.0) return dict.fromkeys(REMOTE_KEYS, 0.0)
+25 -18
View File
@@ -34,8 +34,8 @@ from lerobot.utils.import_utils import _unitree_sdk_available, require_package
from ..robot import Robot from ..robot import Robot
from .config_unitree_g1 import UnitreeG1Config from .config_unitree_g1 import UnitreeG1Config
from .g1_kinematics import G1_29_ArmIK
from .g1_utils import ( from .g1_utils import (
KEYBOARD_KEYS_FIELD,
REMOTE_AXES, REMOTE_AXES,
G1_29_JointArmIndex, G1_29_JointArmIndex,
G1_29_JointIndex, G1_29_JointIndex,
@@ -161,6 +161,9 @@ class UnitreeG1(Robot):
self._client_state_latest: dict[str, float] = {} self._client_state_latest: dict[str, float] = {}
self._client_caps: dict | None = None self._client_caps: dict | None = None
# Optional arm gravity compensation (feed-forward torque via the arm IK solver).
self.arm_ik = G1_29_ArmIK() if config.gravity_compensation else None
# Initialize state variables # Initialize state variables
self.sim_env = None self.sim_env = None
self._env_wrapper = None self._env_wrapper = None
@@ -815,30 +818,34 @@ class UnitreeG1(Robot):
if key.endswith(".q") and key.startswith(arm_prefixes) if key.endswith(".q") and key.startswith(arm_prefixes)
} }
self.publish_lowcmd(action_to_publish) tau = None
if self.config.gravity_compensation and self.arm_ik is not None:
tau = np.zeros(29, dtype=np.float32)
action_np = np.array(
[
action_to_publish.get(f"{joint.name}.q", self.msg.motor_cmd[joint.value].q)
for joint in G1_29_JointArmIndex
],
dtype=np.float32,
)
arm_tau = self.arm_ik.solve_tau(action_np)
arm_start_idx = G1_29_JointArmIndex.kLeftShoulderPitch.value
for joint in G1_29_JointArmIndex:
local_idx = joint.value - arm_start_idx
tau[joint.value] = arm_tau[local_idx]
self.publish_lowcmd(action_to_publish, tau=tau)
return action return action
def _update_controller_action(self, action: RobotAction) -> None: def _update_controller_action(self, action: RobotAction) -> None:
"""Update controller input state from an incoming teleop action. """Update controller input state from an incoming teleop action.
Controller-agnostic: every value-carrying key is forwarded verbatim into Controller-agnostic: every value-carrying key (e.g. locomotion ``remote.*``
``controller_input`` (whole-body ``wb.{i}.pos`` from a 34-D VLA, or whatever a axes/buttons) is forwarded verbatim into ``controller_input`` and each
future controller expects), and each controller extracts only the keys it controller extracts only the keys it understands. The robot deliberately does
understands. The robot deliberately does not enumerate any controller's key not enumerate any controller's key schema here.
schema here.
KeyboardTeleop is the one special case: it emits the currently-pressed keys as
bare action keys with a ``None`` value (``dict.fromkeys(pressed, None)``), so
those are collected into a single held-key set under ``KEYBOARD_KEYS_FIELD``,
rebuilt each tick so releases clear. Special keys arrive as pynput objects and
are normalised to their name ("space", ...).
""" """
with self._controller_action_lock: with self._controller_action_lock:
self.controller_input[KEYBOARD_KEYS_FIELD] = {
(k if isinstance(k, str) else getattr(k, "name", str(k)))
for k, value in action.items()
if value is None
}
for key, value in action.items(): for key, value in action.items():
if isinstance(key, str) and value is not None: if isinstance(key, str) and value is not None:
self.controller_input[key] = value self.controller_input[key] = value