mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-24 18:26:11 +00:00
feat(unitree_g1): read physical wireless remote onboard for locomotion
In onboard mode the laptop client has no robot->laptop feedback, so the physical Unitree remote (whose bytes ride in lowstate) was never parsed and joystick commands never reached the controller. Parse the remote directly from local lowstate in the onboard controller loop; it takes priority over laptop/exo axes when active. Also add axis/button debug logging to both runners. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -100,8 +100,13 @@ def main() -> None:
|
||||
|
||||
robot.send_action(action)
|
||||
n += 1
|
||||
if n % 200 == 0:
|
||||
logger.info("Applied %d actions", n)
|
||||
if n % 60 == 0:
|
||||
axes = {
|
||||
k: round(float(action.get(k, 0.0)), 3)
|
||||
for k in ("remote.lx", "remote.ly", "remote.rx", "remote.ry")
|
||||
}
|
||||
btn = {k: action.get(k) for k in ("remote.button.0", "remote.button.4") if k in action}
|
||||
logger.info("Applied %d actions | axes=%s buttons=%s", n, axes, btn)
|
||||
finally:
|
||||
logger.info("Shutting down onboard controller...")
|
||||
robot.disconnect()
|
||||
|
||||
@@ -101,8 +101,12 @@ def main() -> None:
|
||||
except zmq.Again:
|
||||
pass
|
||||
n += 1
|
||||
if n % 200 == 0:
|
||||
logger.info("Sent %d actions", n)
|
||||
if n % 60 == 0:
|
||||
axes = {
|
||||
k: round(float(action.get(k, 0.0)), 3)
|
||||
for k in ("remote.lx", "remote.ly", "remote.rx", "remote.ry")
|
||||
}
|
||||
logger.info("Sent %d actions | axes=%s", n, axes)
|
||||
if period:
|
||||
time.sleep(max(0.0, period - (time.time() - t0)))
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -53,6 +53,7 @@ if TYPE_CHECKING or _unitree_sdk_available:
|
||||
LowState_ as hg_LowState,
|
||||
)
|
||||
from unitree_sdk2py.utils.crc import CRC
|
||||
from unitree_sdk2py.utils.joystick import Joystick
|
||||
else:
|
||||
_SDKChannelFactoryInitialize = None
|
||||
_SDKChannelPublisher = None
|
||||
@@ -61,6 +62,17 @@ else:
|
||||
hg_LowCmd = None
|
||||
hg_LowState = None
|
||||
CRC = None
|
||||
Joystick = None
|
||||
|
||||
|
||||
# Wireless-remote button byte layout (little-endian uint16 from bytes 2-3), mapped to
|
||||
# the positional button indices the locomotion controllers expect. Mirrors the exo
|
||||
# teleoperator's RemoteController so the onboard path reads the physical Unitree remote
|
||||
# identically.
|
||||
_REMOTE_BUTTON_MAP: list[str] = [
|
||||
"RB", "LB", "start", "back", "RT", "LT", "", "",
|
||||
"A", "B", "X", "Y", "up", "right", "down", "left",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -163,6 +175,10 @@ class UnitreeG1(Robot):
|
||||
self.controller_input = default_remote_input()
|
||||
self.controller_output = {}
|
||||
|
||||
# Onboard-only: parser for the physical Unitree wireless remote (read straight
|
||||
# from local lowstate so joystick locomotion works without a laptop round-trip).
|
||||
self._joystick = None
|
||||
|
||||
def _subscribe_lowstate(self): # polls robot state @ 250Hz
|
||||
while not self._shutdown_event.is_set():
|
||||
start_time = time.time()
|
||||
@@ -277,6 +293,13 @@ class UnitreeG1(Robot):
|
||||
with self._controller_action_lock:
|
||||
controller_input = dict(self.controller_input)
|
||||
|
||||
# Onboard: the physical Unitree remote (in local lowstate) takes
|
||||
# priority for locomotion when active; otherwise laptop/exo axes stand.
|
||||
if self.config.onboard:
|
||||
wl = self._wireless_remote_input(lowstate)
|
||||
if wl is not None:
|
||||
controller_input.update(wl)
|
||||
|
||||
# Run controller step
|
||||
controller_action = self.controller.run_step(controller_input, lowstate)
|
||||
|
||||
@@ -299,6 +322,40 @@ class UnitreeG1(Robot):
|
||||
def configure(self) -> None:
|
||||
pass
|
||||
|
||||
def _wireless_remote_input(self, lowstate) -> dict | None:
|
||||
"""Parse the physical Unitree remote from lowstate into controller inputs.
|
||||
|
||||
Onboard only. Returns None when the remote is idle so the laptop-provided
|
||||
(exo) axes keep control; otherwise the physical remote takes priority — the
|
||||
same precedence the exo teleoperator applies laptop-side.
|
||||
"""
|
||||
js = self._joystick
|
||||
if js is None:
|
||||
return None
|
||||
wr = getattr(lowstate, "wireless_remote", None)
|
||||
if not wr or len(wr) < 24:
|
||||
return None
|
||||
try:
|
||||
js.extract(wr)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
|
||||
axes = {
|
||||
"remote.lx": float(js.lx.data),
|
||||
"remote.ly": float(js.ly.data),
|
||||
"remote.rx": float(js.rx.data),
|
||||
"remote.ry": float(js.ry.data),
|
||||
}
|
||||
active = any(abs(v) > 1e-2 for v in axes.values())
|
||||
out = dict(axes)
|
||||
for i, name in enumerate(_REMOTE_BUTTON_MAP):
|
||||
if name:
|
||||
val = float(getattr(js, name).data)
|
||||
out[f"remote.button.{i}"] = val
|
||||
if val:
|
||||
active = True
|
||||
return out if active else None
|
||||
|
||||
def _release_motion_control(self) -> None:
|
||||
"""Release the robot's built-in motion services so we can send raw lowcmd.
|
||||
|
||||
@@ -338,6 +395,11 @@ class UnitreeG1(Robot):
|
||||
else:
|
||||
self._ChannelFactoryInitialize(0)
|
||||
self._release_motion_control()
|
||||
# Read the physical wireless remote directly from lowstate for locomotion.
|
||||
self._joystick = Joystick()
|
||||
for axis in (self._joystick.lx, self._joystick.ly, self._joystick.rx, self._joystick.ry):
|
||||
axis.smooth = 1.0
|
||||
axis.deadzone = 0.0
|
||||
else:
|
||||
self._ChannelFactoryInitialize(0, config=self.config)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user