ready to review

This commit is contained in:
Martino Russi
2025-11-26 22:00:17 +01:00
parent 3ec332fabc
commit 288cfc7f8e
5 changed files with 70 additions and 81 deletions
@@ -51,4 +51,5 @@ class UnitreeG1Config(RobotConfig):
control_dt = 1.0 / 250.0 # 250Hz control_dt = 1.0 / 250.0 # 250Hz
# socket config for ZMQ bridge
robot_ip: str = "172.18.129.215"
+2 -3
View File
@@ -1,6 +1,5 @@
from enum import IntEnum from enum import IntEnum
class G1_29_JointArmIndex(IntEnum): class G1_29_JointArmIndex(IntEnum):
# Left arm # Left arm
kLeftShoulderPitch = 15 kLeftShoulderPitch = 15
@@ -20,8 +19,8 @@ class G1_29_JointArmIndex(IntEnum):
kRightWristPitch = 27 kRightWristPitch = 27
kRightWristYaw = 28 kRightWristYaw = 28
class G1_29_JointIndex(IntEnum): class G1_29_JointIndex(IntEnum):
# Left leg # Left leg
kLeftHipPitch = 0 kLeftHipPitch = 0
kLeftHipRoll = 1 kLeftHipRoll = 1
@@ -38,7 +37,7 @@ class G1_29_JointIndex(IntEnum):
kRightAnklePitch = 10 kRightAnklePitch = 10
kRightAnkleRoll = 11 kRightAnkleRoll = 11
kWaistYaw = 12 # we're c kWaistYaw = 12
kWaistRoll = 13 kWaistRoll = 13
kWaistPitch = 14 kWaistPitch = 14
+23 -31
View File
@@ -9,29 +9,25 @@ from unitree_sdk2py.core.channel import ChannelFactoryInitialize, ChannelPublish
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_ as hg_LowCmd, LowState_ as hg_LowState from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_ as hg_LowCmd, LowState_ as hg_LowState
from unitree_sdk2py.utils.crc import CRC from unitree_sdk2py.utils.crc import CRC
kTopicLowCommand_Debug = "rt/lowcmd" kTopicLowCommand_Debug = "rt/lowcmd" #action to robot
kTopicLowState = "rt/lowstate" kTopicLowState = "rt/lowstate" #observation from robot
LOWCMD_PORT = 6000 # laptop -> robot LOWCMD_PORT = 6000
LOWSTATE_PORT = 6001 # robot -> laptop LOWSTATE_PORT = 6001
def state_forward_loop(lowstate_sub, lowstate_sock, state_period: float): def state_forward_loop(lowstate_sub, lowstate_sock, state_period: float):#read observation from DDS and send to server
"""
read lowstate from dds and push to laptop at ~state_period.
runs in its own thread.
"""
last_state_time = 0.0 last_state_time = 0.0
while True: while True:
# read from dds (blocking) # read from DDS
msg = lowstate_sub.Read() msg = lowstate_sub.Read()
if msg is None: if msg is None:
continue continue
now = time.time() now = time.time()
# optional downsampling (if robot dds rate > state_period) # optional downsampling (if robot dds rate > state_period)
if now - last_state_time >= state_period: if now - last_state_time >= state_period:
payload = pickle.dumps((kTopicLowState, msg), protocol=pickle.HIGHEST_PROTOCOL) payload = pickle.dumps((kTopicLowState, msg), protocol=pickle.HIGHEST_PROTOCOL)
try: try:
lowstate_sock.send(payload, zmq.NOBLOCK) lowstate_sock.send(payload, zmq.NOBLOCK)
@@ -41,15 +37,11 @@ def state_forward_loop(lowstate_sub, lowstate_sock, state_period: float):
last_state_time = now last_state_time = now
def cmd_forward_loop(lowcmd_sock, lowcmd_pub_debug, crc: CRC): def cmd_forward_loop(lowcmd_sock, lowcmd_pub_debug, crc: CRC):#send action to robot
"""
read lowcmd from laptop (zmq) and push to dds.
runs in its own thread.
"""
while True: while True:
# blocking wait for commands from laptop
payload = lowcmd_sock.recv() payload = lowcmd_sock.recv()
topic, cmd = pickle.loads(payload) # cmd is hg_LowCmd topic, cmd = pickle.loads(payload)
# recompute crc just in case # recompute crc just in case
cmd.crc = crc.Crc(cmd) cmd.crc = crc.Crc(cmd)
@@ -57,15 +49,15 @@ def cmd_forward_loop(lowcmd_sock, lowcmd_pub_debug, crc: CRC):
if topic == kTopicLowCommand_Debug: if topic == kTopicLowCommand_Debug:
lowcmd_pub_debug.Write(cmd) lowcmd_pub_debug.Write(cmd)
else: else:
# ignore unknown topics
pass pass
def main(): def main():
# dds init # initialize DDS
ChannelFactoryInitialize(0) ChannelFactoryInitialize(0)
# acquire motion mode on the robot # stop all active publishers on the robot
msc = MotionSwitcherClient() msc = MotionSwitcherClient()
msc.SetTimeout(5.0) msc.SetTimeout(5.0)
msc.Init() msc.Init()
@@ -78,50 +70,50 @@ def main():
crc = CRC() crc = CRC()
# dds publishers / subscriber # initialize DDS publisher
lowcmd_pub_debug = ChannelPublisher(kTopicLowCommand_Debug, hg_LowCmd) lowcmd_pub_debug = ChannelPublisher(kTopicLowCommand_Debug, hg_LowCmd)
lowcmd_pub_debug.Init() lowcmd_pub_debug.Init()
# initialize DDS subscriber
lowstate_sub = ChannelSubscriber(kTopicLowState, hg_LowState) lowstate_sub = ChannelSubscriber(kTopicLowState, hg_LowState)
lowstate_sub.Init() lowstate_sub.Init()
# zmq setup # initialize ZMQ
ctx = zmq.Context.instance() ctx = zmq.Context.instance()
# commands from laptop # send action to robot
lowcmd_sock = ctx.socket(zmq.PULL) lowcmd_sock = ctx.socket(zmq.PULL)
lowcmd_sock.bind(f"tcp://0.0.0.0:{LOWCMD_PORT}") lowcmd_sock.bind(f"tcp://0.0.0.0:{LOWCMD_PORT}")
# state to laptop # send observation to server
lowstate_sock = ctx.socket(zmq.PUB) lowstate_sock = ctx.socket(zmq.PUB)
lowstate_sock.bind(f"tcp://0.0.0.0:{LOWSTATE_PORT}") lowstate_sock.bind(f"tcp://0.0.0.0:{LOWSTATE_PORT}")
state_period = 0.002 # ~500 hz state_period = 0.002 # ~500 hz
# start threads # start observation forwarding thread
t_state = threading.Thread( t_state = threading.Thread(
target=state_forward_loop, target=state_forward_loop,
args=(lowstate_sub, lowstate_sock, state_period), args=(lowstate_sub, lowstate_sock, state_period),
daemon=True, daemon=True,
) )
t_state.start()
# start action forwarding thread
t_cmd = threading.Thread( t_cmd = threading.Thread(
target=cmd_forward_loop, target=cmd_forward_loop,
args=(lowcmd_sock, lowcmd_pub_debug, crc), args=(lowcmd_sock, lowcmd_pub_debug, crc),
daemon=True, daemon=True,
) )
t_state.start()
t_cmd.start() t_cmd.start()
print("bridge running (lowstate -> zmq, lowcmd -> dds)") print("bridge running (lowstate -> zmq, lowcmd -> dds)")
# keep main thread alive so daemon threads dont exit # keep main thread alive so daemon threads dont exit
try: try:
while True: while True:
time.sleep(1.0) time.sleep(1.0)
except KeyboardInterrupt: except KeyboardInterrupt:
print("shutting down bridge...") print("shutting down bridge...")
# sockets/context will be cleaned up on process exit
if __name__ == "__main__": if __name__ == "__main__":
+20 -6
View File
@@ -1,3 +1,19 @@
#!/usr/bin/env python
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json import json
import logging import logging
import struct import struct
@@ -123,7 +139,10 @@ class UnitreeG1(Robot):
self.subscribe_thread.daemon = True self.subscribe_thread.daemon = True
self.subscribe_thread.start() self.subscribe_thread.start()
while not self.is_connected:
time.sleep(0.1)
logger.warning("[UnitreeG1] Waiting to connect to robot...")
logger.warning("[UnitreeG1] Connected to robot.")
# initialize hg's lowcmd msg # initialize hg's lowcmd msg
self.crc = CRC() self.crc = CRC()
@@ -190,11 +209,6 @@ class UnitreeG1(Robot):
def connect(self, calibrate: bool = True) -> None: #connect to DDS def connect(self, calibrate: bool = True) -> None: #connect to DDS
ChannelFactoryInitialize(0) ChannelFactoryInitialize(0)
while not self.lowstate_buffer.GetData():
time.sleep(0.1)
logger.warning("[UnitreeG1] Waiting to subscribe dds...")
logger.warning("[UnitreeG1] Subscribe dds ok.")
def disconnect(self): def disconnect(self):
pass pass
@@ -1,55 +1,50 @@
# unitree_sdk2_socket.py
import pickle import pickle
import zmq import zmq
# you can tune these or read from env from lerobot.robots.unitree_g1.config_unitree_g1 import UnitreeG1Config
ROBOT_IP = "172.18.129.215"
LOWCMD_PORT = 6000 # laptop -> robot
LOWSTATE_PORT = 6001 # robot -> laptop
_ctx = None _ctx = None
_lowcmd_sock = None _lowcmd_sock = None
_lowstate_sock = None _lowstate_sock = None
LOWCMD_PORT = 6000
LOWSTATE_PORT = 6001
def ChannelFactoryInitialize(*args, **kwargs):
global _ctx, _lowcmd_sock, _lowstate_sock def ChannelFactoryInitialize(*args, **kwargs):#DDS to socket bridge
if _ctx is not None: global _ctx, _lowcmd_sock, _lowstate_sock\
return
# read socket config
config = UnitreeG1Config()
robot_ip = config.robot_ip
_ctx = zmq.Context.instance() _ctx = zmq.Context.instance()
# lowcmd: PUSH from laptop to robot # lowcmd: robot action
_lowcmd_sock = _ctx.socket(zmq.PUSH) _lowcmd_sock = _ctx.socket(zmq.PUSH)
_lowcmd_sock.setsockopt(zmq.CONFLATE, 1) _lowcmd_sock.setsockopt(zmq.CONFLATE, 1)#keep only last message
_lowcmd_sock.connect(f"tcp://{ROBOT_IP}:{LOWCMD_PORT}") _lowcmd_sock.connect(f"tcp://{robot_ip}:{LOWCMD_PORT}")
# lowstate: SUB from robot # lowstate: robot observation
_lowstate_sock = _ctx.socket(zmq.SUB) # no topic filtering _lowstate_sock = _ctx.socket(zmq.SUB)
_lowstate_sock.setsockopt(zmq.CONFLATE, 1) # keep only last message _lowstate_sock.setsockopt(zmq.CONFLATE, 1) # keep only last message
_lowstate_sock.connect(f"tcp://{ROBOT_IP}:{LOWSTATE_PORT}") _lowstate_sock.connect(f"tcp://{robot_ip}:{LOWSTATE_PORT}")
_lowstate_sock.setsockopt_string(zmq.SUBSCRIBE, "") # subscribe to all _lowstate_sock.setsockopt_string(zmq.SUBSCRIBE, "")
class ChannelPublisher: class ChannelPublisher: #send action to robot
# just enough api for your code: __init__, Init, Write
def __init__(self, topic, msg_type): def __init__(self, topic, msg_type):
# we ignore topic/msg_type, the bridge only supports the topics you use
self.topic = topic self.topic = topic
self.msg_type = msg_type self.msg_type = msg_type
def Init(self): def Init(self):
# nothing to do, sockets are global
pass pass
def Write(self, msg): def Write(self, msg):
# msg is hg_LowCmd_ instance we just pickle it _lowcmd_sock.send(pickle.dumps((self.topic, msg)))
payload = pickle.dumps((self.topic, msg))
_lowcmd_sock.send(payload)
class ChannelSubscriber: class ChannelSubscriber: #read observation from robot
# api: __init__, Init, Read
def __init__(self, topic, msg_type): def __init__(self, topic, msg_type):
self.topic = topic self.topic = topic
self.msg_type = msg_type self.msg_type = msg_type
@@ -57,18 +52,6 @@ class ChannelSubscriber:
def Init(self): def Init(self):
pass pass
def Read(self, timeout_ms=None): def Read(self):
"""Block until we get a lowstate, optionally with timeout (ms).""" topic, msg = pickle.loads(_lowstate_sock.recv())
if timeout_ms is None:
payload = _lowstate_sock.recv()
else:
poller = zmq.Poller()
poller.register(_lowstate_sock, zmq.POLLIN)
events = dict(poller.poll(timeout_ms))
if _lowstate_sock not in events:
return None
payload = _lowstate_sock.recv()
topic, msg = pickle.loads(payload)
# you can assert topic == self.topic, but not necessary if you only use one
return msg return msg