mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-29 20:49:42 +00:00
config cleanup
This commit is contained in:
@@ -86,11 +86,6 @@ class UnitreeG1Config(RobotConfig):
|
|||||||
# Compensates for gravity on the unitree's arms using the arm ik solver
|
# Compensates for gravity on the unitree's arms using the arm ik solver
|
||||||
gravity_compensation: bool = False
|
gravity_compensation: bool = False
|
||||||
|
|
||||||
# When False, connect() does not start the background controller thread, so a
|
|
||||||
# caller can drive the controller synchronously (one decode per fed action),
|
|
||||||
# reproducing the deploy's single 50Hz control clock for faithful replay.
|
|
||||||
run_controller_thread: bool = True
|
|
||||||
|
|
||||||
# Token-output VLA interface for the SONIC decoder. When True (and the controller
|
# Token-output VLA interface for the SONIC decoder. When True (and the controller
|
||||||
# is ``SonicWholeBodyController``), the robot advertises a 64-D latent-token action
|
# is ``SonicWholeBodyController``), the robot advertises a 64-D latent-token action
|
||||||
# space (``motion_token.{i}.pos``) and exposes the last commanded token as a 64-D
|
# space (``motion_token.{i}.pos``) and exposes the last commanded token as a 64-D
|
||||||
@@ -103,8 +98,3 @@ class UnitreeG1Config(RobotConfig):
|
|||||||
# Locomotion controller class name, e.g. "GrootLocomotionController",
|
# Locomotion controller class name, e.g. "GrootLocomotionController",
|
||||||
# "HolosomaLocomotionController", or "SonicWholeBodyController". None disables it.
|
# "HolosomaLocomotionController", or "SonicWholeBodyController". None disables it.
|
||||||
controller: str | None = None
|
controller: str | None = None
|
||||||
|
|
||||||
# On disconnect (e.g. Ctrl-C), seconds to hold the current pose while ramping joint
|
|
||||||
# stiffness (kp) to zero — a soft, damped settle instead of an instant limp /
|
|
||||||
# free-fall. 0 disables it (immediate zero-torque). Real robot only.
|
|
||||||
graceful_stop_s: float = 1.5
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import argparse
|
|||||||
import base64
|
import base64
|
||||||
import contextlib
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import signal
|
import signal
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -237,8 +236,7 @@ def serve_onboard_controller(
|
|||||||
cameras={},
|
cameras={},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional camera server (background thread; independent of DDS).
|
# Optional camera server (background daemon thread; independent of DDS).
|
||||||
camera_server = None
|
|
||||||
if cameras:
|
if cameras:
|
||||||
camera_server = ImageServer({"fps": camera_fps, "cameras": cameras}, port=camera_port)
|
camera_server = ImageServer({"fps": camera_fps, "cameras": cameras}, port=camera_port)
|
||||||
threading.Thread(target=camera_server.run, daemon=True).start()
|
threading.Thread(target=camera_server.run, daemon=True).start()
|
||||||
@@ -315,57 +313,9 @@ def serve_onboard_controller(
|
|||||||
if state_sock is not None:
|
if state_sock is not None:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
state_sock.close(linger=0)
|
state_sock.close(linger=0)
|
||||||
if camera_server is not None:
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
camera_server.stop()
|
|
||||||
robot.disconnect()
|
robot.disconnect()
|
||||||
|
|
||||||
|
|
||||||
def parse_camera_specs(spec: str, default_width: int, default_height: int) -> dict[str, dict]:
|
|
||||||
"""Parse a multi-camera spec string into an ImageServer ``cameras`` dict.
|
|
||||||
|
|
||||||
Format: comma-separated ``name:device[:WxH[:FOURCC]]`` entries, e.g.
|
|
||||||
``head_camera:6,left_wrist:0``. ``device`` may be an integer index or an explicit
|
|
||||||
device path (e.g. ``/dev/video6``), including stable ``by-path`` names like
|
|
||||||
``/dev/v4l/by-path/platform-...:2.1:1.3-video-index0`` which survive USB
|
|
||||||
re-enumeration (unlike bare ``/dev/videoN`` indices). Because a by-path name
|
|
||||||
itself contains colons, the optional ``WxH`` and ``FOURCC`` are parsed from the
|
|
||||||
*right* so the device-path colons are preserved.
|
|
||||||
"""
|
|
||||||
wh_re = re.compile(r"\d+x\d+", re.IGNORECASE)
|
|
||||||
fourcc_re = re.compile(r"[A-Za-z0-9]{4}")
|
|
||||||
|
|
||||||
cameras: dict[str, dict] = {}
|
|
||||||
for entry in spec.split(","):
|
|
||||||
entry = entry.strip()
|
|
||||||
if not entry:
|
|
||||||
continue
|
|
||||||
if ":" not in entry:
|
|
||||||
raise ValueError(f"Invalid camera spec '{entry}', expected 'name:device[:WxH[:FOURCC]]'")
|
|
||||||
name, rest = entry.split(":", 1)
|
|
||||||
name = name.strip()
|
|
||||||
tokens = [t.strip() for t in rest.split(":")]
|
|
||||||
|
|
||||||
fourcc = None
|
|
||||||
if len(tokens) >= 3 and wh_re.fullmatch(tokens[-2]) and fourcc_re.fullmatch(tokens[-1]):
|
|
||||||
fourcc = tokens.pop().upper()
|
|
||||||
width, height = default_width, default_height
|
|
||||||
if len(tokens) >= 2 and wh_re.fullmatch(tokens[-1]):
|
|
||||||
w, h = tokens.pop().lower().split("x")
|
|
||||||
width, height = int(w), int(h)
|
|
||||||
|
|
||||||
raw_id = ":".join(tokens).strip()
|
|
||||||
if not raw_id:
|
|
||||||
raise ValueError(f"Invalid camera spec '{entry}', missing device")
|
|
||||||
device_id: int | str = int(raw_id) if raw_id.lstrip("-").isdigit() else raw_id
|
|
||||||
if name in cameras:
|
|
||||||
raise ValueError(f"Duplicate camera name '{name}' in --cameras")
|
|
||||||
cameras[name] = {"device_id": device_id, "shape": [height, width], "fourcc": fourcc}
|
|
||||||
if not cameras:
|
|
||||||
raise ValueError("No cameras parsed from --cameras spec")
|
|
||||||
return cameras
|
|
||||||
|
|
||||||
|
|
||||||
def lowstate_to_dict(msg: hg_LowState) -> dict[str, Any]:
|
def lowstate_to_dict(msg: hg_LowState) -> dict[str, Any]:
|
||||||
"""Convert LowState SDK message to a JSON-serializable dictionary."""
|
"""Convert LowState SDK message to a JSON-serializable dictionary."""
|
||||||
motor_states = []
|
motor_states = []
|
||||||
@@ -470,11 +420,7 @@ def main() -> None:
|
|||||||
"""Main entry point for the robot server bridge."""
|
"""Main entry point for the robot server bridge."""
|
||||||
parser = argparse.ArgumentParser(description="DDS-to-ZMQ bridge server for Unitree G1")
|
parser = argparse.ArgumentParser(description="DDS-to-ZMQ bridge server for Unitree G1")
|
||||||
parser.add_argument("--camera", action="store_true", help="Also launch camera server")
|
parser.add_argument("--camera", action="store_true", help="Also launch camera server")
|
||||||
parser.add_argument("--camera-device", default="4",
|
parser.add_argument("--camera-device", type=int, default=4, help="Camera device ID (default: 4)")
|
||||||
help="Camera device: index or /dev/video path or by-path name (default: 4)")
|
|
||||||
parser.add_argument("--cameras", default=None,
|
|
||||||
help="Multi-camera spec 'name:device[:WxH[:FOURCC]]', comma-separated. Overrides "
|
|
||||||
"--camera-device; device may be a by-path name to survive USB re-enumeration.")
|
|
||||||
parser.add_argument("--camera-fps", type=int, default=30, help="Camera FPS (default: 30)")
|
parser.add_argument("--camera-fps", type=int, default=30, help="Camera FPS (default: 30)")
|
||||||
parser.add_argument("--camera-width", type=int, default=640, help="Camera width (default: 640)")
|
parser.add_argument("--camera-width", type=int, default=640, help="Camera width (default: 640)")
|
||||||
parser.add_argument("--camera-height", type=int, default=480, help="Camera height (default: 480)")
|
parser.add_argument("--camera-height", type=int, default=480, help="Camera height (default: 480)")
|
||||||
@@ -542,13 +488,13 @@ def main() -> None:
|
|||||||
print(f"[handshake] running controller ONBOARD: {agreed['controller']} "
|
print(f"[handshake] running controller ONBOARD: {agreed['controller']} "
|
||||||
f"(sonic_token_action={agreed['sonic_token_action']})")
|
f"(sonic_token_action={agreed['sonic_token_action']})")
|
||||||
cameras = None
|
cameras = None
|
||||||
if args.camera or args.cameras:
|
if args.camera:
|
||||||
if args.cameras:
|
cameras = {
|
||||||
cameras = parse_camera_specs(args.cameras, args.camera_width, args.camera_height)
|
"head_camera": {
|
||||||
else:
|
"device_id": args.camera_device,
|
||||||
dev = args.camera_device
|
"shape": [args.camera_height, args.camera_width],
|
||||||
dev = int(dev) if str(dev).lstrip("-").isdigit() else dev
|
}
|
||||||
cameras = {"head_camera": {"device_id": dev, "shape": [args.camera_height, args.camera_width]}}
|
}
|
||||||
serve_onboard_controller(
|
serve_onboard_controller(
|
||||||
controller=agreed["controller"],
|
controller=agreed["controller"],
|
||||||
sonic_token_action=bool(agreed["sonic_token_action"]),
|
sonic_token_action=bool(agreed["sonic_token_action"]),
|
||||||
@@ -561,20 +507,20 @@ def main() -> None:
|
|||||||
|
|
||||||
# Optionally start camera server in background thread
|
# Optionally start camera server in background thread
|
||||||
camera_thread = None
|
camera_thread = None
|
||||||
if args.camera or args.cameras:
|
if args.camera:
|
||||||
if args.cameras:
|
camera_config = {
|
||||||
cameras = parse_camera_specs(args.cameras, args.camera_width, args.camera_height)
|
"fps": args.camera_fps,
|
||||||
else:
|
"cameras": {
|
||||||
# Single camera; accept an int index or a device/by-path string.
|
"head_camera": {
|
||||||
dev = args.camera_device
|
"device_id": args.camera_device,
|
||||||
dev = int(dev) if str(dev).lstrip("-").isdigit() else dev
|
"shape": [args.camera_height, args.camera_width],
|
||||||
cameras = {"head_camera": {"device_id": dev, "shape": [args.camera_height, args.camera_width]}}
|
}
|
||||||
camera_config = {"fps": args.camera_fps, "cameras": cameras}
|
},
|
||||||
|
}
|
||||||
camera_server = ImageServer(camera_config, port=args.camera_port)
|
camera_server = ImageServer(camera_config, port=args.camera_port)
|
||||||
camera_thread = threading.Thread(target=camera_server.run, daemon=True)
|
camera_thread = threading.Thread(target=camera_server.run, daemon=True)
|
||||||
camera_thread.start()
|
camera_thread.start()
|
||||||
cam_summary = ", ".join(f"{n}(dev {c['device_id']})" for n, c in cameras.items())
|
print(f"Camera server started on port {args.camera_port} (device {args.camera_device})")
|
||||||
print(f"Camera server started on port {args.camera_port}: {cam_summary}")
|
|
||||||
|
|
||||||
# initialize DDS
|
# initialize DDS
|
||||||
ChannelFactoryInitialize(0)
|
ChannelFactoryInitialize(0)
|
||||||
|
|||||||
@@ -650,16 +650,13 @@ class UnitreeG1(Robot):
|
|||||||
self.msg.motor_cmd[joint].kd = self.kd[joint.value]
|
self.msg.motor_cmd[joint].kd = self.kd[joint.value]
|
||||||
self.msg.motor_cmd[joint].q = lowstate.motor_state[joint.value].q
|
self.msg.motor_cmd[joint].q = lowstate.motor_state[joint.value].q
|
||||||
|
|
||||||
# Start controller thread if enabled. Skipped when run_controller_thread is
|
# Start the 50 Hz controller thread (runs the locomotion/whole-body policy and
|
||||||
# False so a caller can step the controller synchronously (faithful replay).
|
# publishes low commands to DDS).
|
||||||
if self.controller is not None and self.config.run_controller_thread:
|
if self.controller is not None:
|
||||||
self._controller_thread = threading.Thread(target=self._controller_loop, daemon=True)
|
self._controller_thread = threading.Thread(target=self._controller_loop, daemon=True)
|
||||||
self._controller_thread.start()
|
self._controller_thread.start()
|
||||||
fps = int(1.0 / self.controller.control_dt)
|
fps = int(1.0 / self.controller.control_dt)
|
||||||
logger.info(f"Controller thread started ({fps}Hz)")
|
logger.info(f"Controller thread started ({fps}Hz)")
|
||||||
elif self.controller is not None:
|
|
||||||
logger.info("Controller thread disabled (run_controller_thread=False); "
|
|
||||||
"caller must drive controller.run_step synchronously.")
|
|
||||||
|
|
||||||
def _send_zero_torque(self) -> None:
|
def _send_zero_torque(self) -> None:
|
||||||
"""Send a zero-gain command to make joints passive before shutting down."""
|
"""Send a zero-gain command to make joints passive before shutting down."""
|
||||||
@@ -675,35 +672,6 @@ class UnitreeG1(Robot):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to send zero-torque on disconnect: {e}")
|
logger.warning(f"Failed to send zero-torque on disconnect: {e}")
|
||||||
|
|
||||||
def _graceful_stop(self) -> None:
|
|
||||||
"""Soft shutdown: hold the current pose and ramp joint stiffness (kp) to zero
|
|
||||||
over ``graceful_stop_s`` while keeping damping (kd), then go passive.
|
|
||||||
|
|
||||||
Prevents the robot from collapsing the instant control ends (a bare
|
|
||||||
zero-torque command is kp=kd=0 ≈ free-fall). Must run after the controller
|
|
||||||
loop has stopped so the two aren't publishing at once.
|
|
||||||
"""
|
|
||||||
if self.config.graceful_stop_s <= 0:
|
|
||||||
self._send_zero_torque()
|
|
||||||
return
|
|
||||||
with self._lowstate_lock:
|
|
||||||
lowstate = self._lowstate
|
|
||||||
if lowstate is None:
|
|
||||||
self._send_zero_torque()
|
|
||||||
return
|
|
||||||
q_hold = {f"{motor.name}.q": lowstate.motor_state[motor.value].q for motor in G1_29_JointIndex}
|
|
||||||
kp = np.array(self.kp, dtype=np.float32)
|
|
||||||
kd = np.array(self.kd, dtype=np.float32)
|
|
||||||
zeros = np.zeros(29, dtype=np.float32)
|
|
||||||
dt = self.controller.control_dt if self.controller is not None else self.config.control_dt
|
|
||||||
steps = max(1, int(self.config.graceful_stop_s / dt))
|
|
||||||
logger.info("Graceful stop: damping down over %.1fs", self.config.graceful_stop_s)
|
|
||||||
for i in range(steps):
|
|
||||||
ratio = (i + 1) / steps
|
|
||||||
self.publish_lowcmd(q_hold, kp=kp * (1.0 - ratio), kd=kd, tau=zeros)
|
|
||||||
time.sleep(dt)
|
|
||||||
self._send_zero_torque()
|
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
if self._client:
|
if self._client:
|
||||||
self._disconnect_client()
|
self._disconnect_client()
|
||||||
@@ -724,11 +692,12 @@ class UnitreeG1(Robot):
|
|||||||
"concurrent low commands (fail-safe: joints keep last command until exit)"
|
"concurrent low commands (fail-safe: joints keep last command until exit)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Soft, damped settle instead of an instant limp (real robot only; the
|
# Put the robot in passive mode (zero-torque) before stopping the rest (real
|
||||||
# subscribe thread is still alive here to supply the current pose). Only ramp
|
# robot only; the subscribe thread is still alive here to supply the current
|
||||||
# once the controller thread has definitely exited.
|
# pose). Only publish once the controller thread has definitely exited so the
|
||||||
|
# two aren't publishing at once.
|
||||||
if not self.config.is_simulation and controller_stopped:
|
if not self.config.is_simulation and controller_stopped:
|
||||||
self._graceful_stop()
|
self._send_zero_torque()
|
||||||
|
|
||||||
if self.controller is not None and hasattr(self.controller, "shutdown"):
|
if self.controller is not None and hasattr(self.controller, "shutdown"):
|
||||||
self.controller.shutdown()
|
self.controller.shutdown()
|
||||||
|
|||||||
Reference in New Issue
Block a user