hex-zmq-servers 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hex_zmq_servers/__init__.py +173 -0
- hex_zmq_servers/cam/__init__.py +52 -0
- hex_zmq_servers/cam/berxel/__init__.py +17 -0
- hex_zmq_servers/cam/berxel/cam_berxel.py +282 -0
- hex_zmq_servers/cam/berxel/cam_berxel_cli.py +33 -0
- hex_zmq_servers/cam/berxel/cam_berxel_srv.py +79 -0
- hex_zmq_servers/cam/cam_base.py +189 -0
- hex_zmq_servers/cam/dummy/__init__.py +17 -0
- hex_zmq_servers/cam/dummy/cam_dummy.py +69 -0
- hex_zmq_servers/cam/dummy/cam_dummy_cli.py +29 -0
- hex_zmq_servers/cam/dummy/cam_dummy_srv.py +68 -0
- hex_zmq_servers/cam/realsense/__init__.py +17 -0
- hex_zmq_servers/cam/realsense/cam_realsense.py +159 -0
- hex_zmq_servers/cam/realsense/cam_realsense_cli.py +33 -0
- hex_zmq_servers/cam/realsense/cam_realsense_srv.py +78 -0
- hex_zmq_servers/cam/rgb/__init__.py +17 -0
- hex_zmq_servers/cam/rgb/cam_rgb.py +135 -0
- hex_zmq_servers/cam/rgb/cam_rgb_cli.py +43 -0
- hex_zmq_servers/cam/rgb/cam_rgb_srv.py +78 -0
- hex_zmq_servers/config/cam_berxel.json +18 -0
- hex_zmq_servers/config/cam_dummy.json +12 -0
- hex_zmq_servers/config/cam_realsense.json +17 -0
- hex_zmq_servers/config/cam_rgb.json +28 -0
- hex_zmq_servers/config/mujoco_archer_y6.json +37 -0
- hex_zmq_servers/config/mujoco_e3_desktop.json +41 -0
- hex_zmq_servers/config/robot_dummy.json +153 -0
- hex_zmq_servers/config/robot_gello.json +66 -0
- hex_zmq_servers/config/robot_hexarm.json +37 -0
- hex_zmq_servers/config/zmq_dummy.json +12 -0
- hex_zmq_servers/device_base.py +44 -0
- hex_zmq_servers/hex_launch.py +489 -0
- hex_zmq_servers/mujoco/__init__.py +28 -0
- hex_zmq_servers/mujoco/archer_y6/__init__.py +17 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/arm_base_link.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_1.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_2.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_3.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_4.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_5.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/assets.xml +17 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/camera_link.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_base_link.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_left_helper_link.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_left_link_1.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_left_link_2.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_right_helper_link.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_right_link_1.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_right_link_2.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/assets/table_link.STL +0 -0
- hex_zmq_servers/mujoco/archer_y6/model/robot.xml +95 -0
- hex_zmq_servers/mujoco/archer_y6/model/scene.xml +51 -0
- hex_zmq_servers/mujoco/archer_y6/model/setting.xml +37 -0
- hex_zmq_servers/mujoco/archer_y6/mujoco_archer_y6.py +325 -0
- hex_zmq_servers/mujoco/archer_y6/mujoco_archer_y6_cli.py +71 -0
- hex_zmq_servers/mujoco/archer_y6/mujoco_archer_y6_srv.py +148 -0
- hex_zmq_servers/mujoco/e3_desktop/__init__.py +17 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_base_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_1.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_2.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_3.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_4.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_5.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/assets.xml +18 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/camera_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/e3_desktop_base_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_base_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_left_helper_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_left_link_1.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_left_link_2.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_right_helper_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_right_link_1.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_right_link_2.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/assets/table_link.STL +0 -0
- hex_zmq_servers/mujoco/e3_desktop/model/robot.xml +188 -0
- hex_zmq_servers/mujoco/e3_desktop/model/scene.xml +53 -0
- hex_zmq_servers/mujoco/e3_desktop/model/setting.xml +72 -0
- hex_zmq_servers/mujoco/e3_desktop/mujoco_e3_desktop.py +449 -0
- hex_zmq_servers/mujoco/e3_desktop/mujoco_e3_desktop_cli.py +289 -0
- hex_zmq_servers/mujoco/e3_desktop/mujoco_e3_desktop_srv.py +244 -0
- hex_zmq_servers/mujoco/mujoco_base.py +425 -0
- hex_zmq_servers/robot/__init__.py +37 -0
- hex_zmq_servers/robot/dummy/__init__.py +17 -0
- hex_zmq_servers/robot/dummy/robot_dummy.py +94 -0
- hex_zmq_servers/robot/dummy/robot_dummy_cli.py +29 -0
- hex_zmq_servers/robot/dummy/robot_dummy_srv.py +82 -0
- hex_zmq_servers/robot/gello/__init__.py +17 -0
- hex_zmq_servers/robot/gello/robot_gello.py +366 -0
- hex_zmq_servers/robot/gello/robot_gello_cli.py +29 -0
- hex_zmq_servers/robot/gello/robot_gello_srv.py +93 -0
- hex_zmq_servers/robot/hexarm/__init__.py +47 -0
- hex_zmq_servers/robot/hexarm/robot_hexarm.py +292 -0
- hex_zmq_servers/robot/hexarm/robot_hexarm_cli.py +37 -0
- hex_zmq_servers/robot/hexarm/robot_hexarm_srv.py +87 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_l6y/empty.urdf +206 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100.urdf +206 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100_handle.urdf +206 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100_p050.urdf +206 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100_p050_handle.urdf +206 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_y6/empty.urdf +207 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100.urdf +207 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100_handle.urdf +207 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100_p050.urdf +207 -0
- hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100_p050_handle.urdf +207 -0
- hex_zmq_servers/robot/robot_base.py +276 -0
- hex_zmq_servers/zmq_base.py +547 -0
- hex_zmq_servers-0.3.9.dist-info/METADATA +147 -0
- hex_zmq_servers-0.3.9.dist-info/RECORD +110 -0
- hex_zmq_servers-0.3.9.dist-info/WHEEL +5 -0
- hex_zmq_servers-0.3.9.dist-info/licenses/LICENSE +201 -0
- hex_zmq_servers-0.3.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding:utf-8 -*-
|
|
3
|
+
################################################################
|
|
4
|
+
# Copyright 2025 Dong Zhaorui. All rights reserved.
|
|
5
|
+
# Author: Dong Zhaorui 847235539@qq.com
|
|
6
|
+
# Date : 2025-09-12
|
|
7
|
+
################################################################
|
|
8
|
+
|
|
9
|
+
import os, signal, json
|
|
10
|
+
import time, ctypes, ctypes.util
|
|
11
|
+
import threading
|
|
12
|
+
import zmq
|
|
13
|
+
import numpy as np
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
|
|
16
|
+
MAX_SEQ_NUM = int(1e12)
|
|
17
|
+
MAX_DEQUE_LEN = 10
|
|
18
|
+
|
|
19
|
+
################################################################
|
|
20
|
+
# Time Related
|
|
21
|
+
################################################################
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SingletonMeta(type):
|
|
25
|
+
_instances = {}
|
|
26
|
+
|
|
27
|
+
def __call__(cls, *args, **kwargs):
|
|
28
|
+
if cls not in cls._instances:
|
|
29
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
|
30
|
+
return cls._instances[cls]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HexTimeManager(metaclass=SingletonMeta):
|
|
34
|
+
|
|
35
|
+
class timespec(ctypes.Structure):
|
|
36
|
+
_fields_ = [("tv_sec", ctypes.c_long), ("tv_nsec", ctypes.c_long)]
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self.__use_ptp = False
|
|
40
|
+
ptp_path = os.getenv("HEX_PTP_CLOCK", None)
|
|
41
|
+
if ptp_path is not None:
|
|
42
|
+
self.__fd = os.open(ptp_path, os.O_RDONLY | os.O_CLOEXEC)
|
|
43
|
+
self.__clock_id = ((~self.__fd) << 3) | 3
|
|
44
|
+
self.__libc = ctypes.CDLL(
|
|
45
|
+
ctypes.util.find_library("c"),
|
|
46
|
+
use_errno=True,
|
|
47
|
+
)
|
|
48
|
+
self.__use_ptp = True
|
|
49
|
+
print(f"Using PTP clock from {ptp_path}")
|
|
50
|
+
else:
|
|
51
|
+
print("Using system clock")
|
|
52
|
+
|
|
53
|
+
def __del__(self):
|
|
54
|
+
if self.__use_ptp:
|
|
55
|
+
os.close(self.__fd)
|
|
56
|
+
|
|
57
|
+
def get_now_ns(self) -> int:
|
|
58
|
+
if self.__use_ptp:
|
|
59
|
+
ts = self.timespec()
|
|
60
|
+
if self.__libc.clock_gettime(self.__clock_id,
|
|
61
|
+
ctypes.byref(ts)) != 0:
|
|
62
|
+
err = ctypes.get_errno()
|
|
63
|
+
raise OSError(err, os.strerror(err))
|
|
64
|
+
return ts.tv_sec * 1_000_000_000 + ts.tv_nsec
|
|
65
|
+
else:
|
|
66
|
+
return time.perf_counter_ns()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_HEX_TIME_MANAGER = HexTimeManager()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def hex_zmq_ts_to_ns(ts: dict) -> int:
|
|
73
|
+
return ts['s'] * 1_000_000_000 + ts['ns']
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ns_to_hex_zmq_ts(ns: int) -> dict:
|
|
77
|
+
return {
|
|
78
|
+
"s": ns // 1_000_000_000,
|
|
79
|
+
"ns": ns % 1_000_000_000,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def hex_ns_now() -> int:
|
|
84
|
+
return _HEX_TIME_MANAGER.get_now_ns()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def hex_zmq_ts_now() -> dict:
|
|
88
|
+
return ns_to_hex_zmq_ts(hex_ns_now())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def hex_zmq_ts_delta_ms(curr_ts, hdr_ts) -> float:
|
|
92
|
+
try:
|
|
93
|
+
return (curr_ts['s'] - hdr_ts['s']) * 1_000 + (
|
|
94
|
+
curr_ts['ns'] - hdr_ts['ns']) / 1_000_000
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"hex_zmq_ts_delta_ms failed: {e}")
|
|
98
|
+
return np.inf
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class HexRate:
|
|
102
|
+
|
|
103
|
+
def __init__(self, hz: float, spin_threshold_ns: int = 10_000):
|
|
104
|
+
if hz <= 0:
|
|
105
|
+
raise ValueError("hz must be greater than 0")
|
|
106
|
+
if spin_threshold_ns < 0:
|
|
107
|
+
raise ValueError("spin_threshold_ns must be non-negative")
|
|
108
|
+
self.__period_ns = int(1_000_000_000 / hz)
|
|
109
|
+
self.__next_ns = self.__now_ns() + self.__period_ns
|
|
110
|
+
self.__spin_threshold_ns = spin_threshold_ns
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def __now_ns() -> int:
|
|
114
|
+
return hex_ns_now()
|
|
115
|
+
|
|
116
|
+
def reset(self):
|
|
117
|
+
self.__next_ns = self.__now_ns() + self.__period_ns
|
|
118
|
+
|
|
119
|
+
def sleep(self):
|
|
120
|
+
target_ns = self.__next_ns
|
|
121
|
+
now_ns = self.__now_ns()
|
|
122
|
+
remain_ns = target_ns - now_ns
|
|
123
|
+
if remain_ns <= 0:
|
|
124
|
+
needed_period = (now_ns - target_ns) // self.__period_ns + 1
|
|
125
|
+
self.__next_ns += needed_period * self.__period_ns
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
spin_threshold = min(self.__spin_threshold_ns, self.__period_ns)
|
|
129
|
+
coarse_sleep_ns = remain_ns - spin_threshold
|
|
130
|
+
if coarse_sleep_ns > 0:
|
|
131
|
+
time.sleep(coarse_sleep_ns / 1_000_000_000.0)
|
|
132
|
+
|
|
133
|
+
while True:
|
|
134
|
+
now_ns = self.__now_ns()
|
|
135
|
+
if now_ns >= target_ns:
|
|
136
|
+
break
|
|
137
|
+
if target_ns - now_ns > 50_000:
|
|
138
|
+
time.sleep(0)
|
|
139
|
+
|
|
140
|
+
self.__next_ns += self.__period_ns
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
################################################################
|
|
144
|
+
# ZMQ Related
|
|
145
|
+
################################################################
|
|
146
|
+
|
|
147
|
+
NET_CONFIG = {
|
|
148
|
+
"ip": "127.0.0.1",
|
|
149
|
+
"port": 12345,
|
|
150
|
+
"realtime_mode": False,
|
|
151
|
+
"deque_maxlen": 10,
|
|
152
|
+
"client_timeout_ms": 200,
|
|
153
|
+
"server_timeout_ms": 1_000,
|
|
154
|
+
"server_num_workers": 4,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class HexZMQClientBase(ABC):
|
|
159
|
+
|
|
160
|
+
def __init__(self, net_config: dict = NET_CONFIG):
|
|
161
|
+
self._max_seq_num = MAX_SEQ_NUM
|
|
162
|
+
self._realtime_mode = net_config.get("realtime_mode", False)
|
|
163
|
+
self._deque_maxlen = max(
|
|
164
|
+
1,
|
|
165
|
+
net_config.get("deque_maxlen", MAX_DEQUE_LEN),
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
port = net_config["port"]
|
|
169
|
+
ip = net_config["ip"]
|
|
170
|
+
client_timeout_ms = net_config["client_timeout_ms"]
|
|
171
|
+
except KeyError as ke:
|
|
172
|
+
missing_key = ke.args[0]
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"net_config is not valid, missing key: {missing_key}")
|
|
175
|
+
|
|
176
|
+
self._context = zmq.Context().instance()
|
|
177
|
+
self._ip = ip
|
|
178
|
+
self._port = port
|
|
179
|
+
self._timeout_ms = client_timeout_ms
|
|
180
|
+
self._socket = None
|
|
181
|
+
self._lock = threading.Lock()
|
|
182
|
+
self.__make_socket()
|
|
183
|
+
|
|
184
|
+
# receive thread
|
|
185
|
+
self._recv_thread = threading.Thread(
|
|
186
|
+
target=self._recv_loop,
|
|
187
|
+
daemon=True,
|
|
188
|
+
)
|
|
189
|
+
self._recv_flag = False
|
|
190
|
+
|
|
191
|
+
def __del__(self):
|
|
192
|
+
self.close()
|
|
193
|
+
|
|
194
|
+
def __make_socket(self):
|
|
195
|
+
if self._socket is not None:
|
|
196
|
+
try:
|
|
197
|
+
self._socket.close(0)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
new_socket = self._context.socket(zmq.REQ)
|
|
202
|
+
new_socket.setsockopt(zmq.LINGER, 0)
|
|
203
|
+
new_socket.setsockopt(zmq.RCVTIMEO, self._timeout_ms)
|
|
204
|
+
new_socket.setsockopt(zmq.SNDTIMEO, self._timeout_ms)
|
|
205
|
+
new_socket.setsockopt(zmq.IMMEDIATE, 1)
|
|
206
|
+
new_socket.setsockopt(zmq.TCP_KEEPALIVE, 1)
|
|
207
|
+
new_socket.connect(f"tcp://{self._ip}:{self._port}")
|
|
208
|
+
self._socket = new_socket
|
|
209
|
+
|
|
210
|
+
def request(self, req_dict: dict, req_buf: np.ndarray | None = None):
|
|
211
|
+
with self._lock:
|
|
212
|
+
try:
|
|
213
|
+
self.__send_req(req_dict, req_buf)
|
|
214
|
+
except zmq.Again:
|
|
215
|
+
print("client send failed; recreate socket")
|
|
216
|
+
self.__make_socket()
|
|
217
|
+
return None, None
|
|
218
|
+
|
|
219
|
+
resp_hdr, resp_buf = self.__recv_resp()
|
|
220
|
+
if resp_hdr is None:
|
|
221
|
+
print("client recv failed; recreate socket")
|
|
222
|
+
self.__make_socket()
|
|
223
|
+
return resp_hdr, resp_buf
|
|
224
|
+
|
|
225
|
+
def is_working(self) -> bool:
|
|
226
|
+
working_hdr, _ = self.request({"cmd": "is_working"})
|
|
227
|
+
if working_hdr is None:
|
|
228
|
+
return False
|
|
229
|
+
else:
|
|
230
|
+
return working_hdr["cmd"] == "is_working_ok"
|
|
231
|
+
|
|
232
|
+
def __send_req(self, req_dict: dict, req_buf: np.ndarray | None = None):
|
|
233
|
+
# construct send header
|
|
234
|
+
if not "cmd" in req_dict:
|
|
235
|
+
raise ValueError("`cmd` is required")
|
|
236
|
+
if req_buf is None:
|
|
237
|
+
req_buf = np.zeros(0, dtype=np.uint8)
|
|
238
|
+
if not req_buf.flags.c_contiguous:
|
|
239
|
+
req_buf = np.ascontiguousarray(req_buf)
|
|
240
|
+
send_hdr = {
|
|
241
|
+
"cmd": req_dict["cmd"],
|
|
242
|
+
"ts": req_dict.get("ts", hex_zmq_ts_now()),
|
|
243
|
+
"args": req_dict.get("args", None),
|
|
244
|
+
"dtype": str(req_buf.dtype),
|
|
245
|
+
"shape": tuple(req_buf.shape),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
self._socket.send_multipart(
|
|
250
|
+
[json.dumps(send_hdr).encode("utf-8"),
|
|
251
|
+
memoryview(req_buf)],
|
|
252
|
+
copy=(req_buf.nbytes < 65536),
|
|
253
|
+
)
|
|
254
|
+
except zmq.Again:
|
|
255
|
+
print("client send failed")
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
def __recv_resp(self):
|
|
259
|
+
try:
|
|
260
|
+
frames = self._socket.recv_multipart()
|
|
261
|
+
if len(frames) != 2:
|
|
262
|
+
raise ValueError("invalid response")
|
|
263
|
+
send_hdr_bytes, raw_buf = frames
|
|
264
|
+
resp_hdr = json.loads(send_hdr_bytes)
|
|
265
|
+
resp_buf = np.frombuffer(
|
|
266
|
+
raw_buf,
|
|
267
|
+
dtype=np.dtype(resp_hdr["dtype"]),
|
|
268
|
+
).reshape(
|
|
269
|
+
tuple(resp_hdr["shape"]),
|
|
270
|
+
order="C",
|
|
271
|
+
)
|
|
272
|
+
return resp_hdr, resp_buf
|
|
273
|
+
except zmq.Again:
|
|
274
|
+
return None, None
|
|
275
|
+
|
|
276
|
+
def close(self):
|
|
277
|
+
self._recv_flag = False
|
|
278
|
+
self._recv_thread.join()
|
|
279
|
+
if self._socket is not None:
|
|
280
|
+
try:
|
|
281
|
+
self._socket.close(0)
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
def _wait_for_working(self, timeout: float = 5.0):
|
|
286
|
+
for _ in range(int(timeout * 10)):
|
|
287
|
+
if self.is_working():
|
|
288
|
+
if hasattr(self, "seq_clear"):
|
|
289
|
+
self.seq_clear()
|
|
290
|
+
break
|
|
291
|
+
else:
|
|
292
|
+
time.sleep(0.1)
|
|
293
|
+
self._recv_flag = True
|
|
294
|
+
self._recv_thread.start()
|
|
295
|
+
|
|
296
|
+
@abstractmethod
|
|
297
|
+
def _recv_loop(self):
|
|
298
|
+
raise NotImplementedError(
|
|
299
|
+
"`_receive_thread` should be implemented by the child class")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class HexZMQServerBase(ABC):
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
net_config: dict = NET_CONFIG,
|
|
307
|
+
):
|
|
308
|
+
self._max_seq_num = MAX_SEQ_NUM
|
|
309
|
+
self._realtime_mode = net_config.get("realtime_mode", False)
|
|
310
|
+
self._deque_maxlen = max(
|
|
311
|
+
1,
|
|
312
|
+
net_config.get("deque_maxlen", MAX_DEQUE_LEN),
|
|
313
|
+
)
|
|
314
|
+
try:
|
|
315
|
+
port = net_config["port"]
|
|
316
|
+
ip = net_config["ip"]
|
|
317
|
+
num_workers = net_config["server_num_workers"]
|
|
318
|
+
timeout_ms = net_config["server_timeout_ms"]
|
|
319
|
+
except KeyError as ke:
|
|
320
|
+
missing_key = ke.args[0]
|
|
321
|
+
raise ValueError(
|
|
322
|
+
f"net_config is not valid, missing key: {missing_key}")
|
|
323
|
+
|
|
324
|
+
self._stop_event = threading.Event()
|
|
325
|
+
self._num_workers = max(1, min(num_workers, os.cpu_count()))
|
|
326
|
+
self._timeout_ms = timeout_ms
|
|
327
|
+
|
|
328
|
+
self._context = zmq.Context().instance()
|
|
329
|
+
self._frontend = self._context.socket(zmq.ROUTER)
|
|
330
|
+
self._frontend.setsockopt(zmq.LINGER, 0)
|
|
331
|
+
self._frontend.setsockopt(zmq.TCP_KEEPALIVE, 1)
|
|
332
|
+
self._frontend.bind(f"tcp://{ip}:{port}")
|
|
333
|
+
|
|
334
|
+
self._backend = self._context.socket(zmq.DEALER)
|
|
335
|
+
self._backend.setsockopt(zmq.LINGER, 0)
|
|
336
|
+
self._backend.bind(f"inproc://hex_workers")
|
|
337
|
+
|
|
338
|
+
self._workers: list[threading.Thread] = []
|
|
339
|
+
self._proxy_thread: threading.Thread | None = None
|
|
340
|
+
|
|
341
|
+
def __del__(self):
|
|
342
|
+
self.close()
|
|
343
|
+
|
|
344
|
+
def _single_thread(self, worker_id: int):
|
|
345
|
+
socket = self._context.socket(zmq.REP)
|
|
346
|
+
socket.setsockopt(zmq.LINGER, 0)
|
|
347
|
+
socket.setsockopt(zmq.RCVTIMEO, self._timeout_ms)
|
|
348
|
+
socket.connect(f"inproc://hex_workers")
|
|
349
|
+
|
|
350
|
+
while not self._stop_event.is_set():
|
|
351
|
+
try:
|
|
352
|
+
frames = socket.recv_multipart()
|
|
353
|
+
except zmq.Again:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
if len(frames) != 2:
|
|
358
|
+
raise ValueError("invalid request")
|
|
359
|
+
send_hdr_bytes, raw_buf = frames
|
|
360
|
+
req_hdr = json.loads(send_hdr_bytes)
|
|
361
|
+
req_buf = np.frombuffer(
|
|
362
|
+
raw_buf,
|
|
363
|
+
dtype=np.dtype(req_hdr["dtype"])).reshape(req_hdr["shape"],
|
|
364
|
+
order="C")
|
|
365
|
+
|
|
366
|
+
resp_hdr, resp_buf = self._process_request(req_hdr, req_buf)
|
|
367
|
+
|
|
368
|
+
if resp_buf is None:
|
|
369
|
+
resp_buf = np.zeros(0, dtype=np.uint8)
|
|
370
|
+
if not resp_buf.flags.c_contiguous:
|
|
371
|
+
resp_buf = np.ascontiguousarray(resp_buf)
|
|
372
|
+
send_hdr = {
|
|
373
|
+
"cmd": resp_hdr["cmd"],
|
|
374
|
+
"ts": resp_hdr.get("ts", hex_zmq_ts_now()),
|
|
375
|
+
"args": resp_hdr.get("args", None),
|
|
376
|
+
"dtype": str(resp_buf.dtype),
|
|
377
|
+
"shape": tuple(resp_buf.shape),
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
socket.send_multipart([
|
|
381
|
+
json.dumps(send_hdr).encode("utf-8"),
|
|
382
|
+
memoryview(resp_buf)
|
|
383
|
+
],
|
|
384
|
+
copy=(resp_buf.nbytes < 65536))
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
err_hdr = {
|
|
388
|
+
"cmd": (f"{req_hdr.get('cmd')}_error"
|
|
389
|
+
if isinstance(req_hdr, dict) and "cmd" in req_hdr
|
|
390
|
+
else "error"),
|
|
391
|
+
"args": {
|
|
392
|
+
"err": str(e)
|
|
393
|
+
},
|
|
394
|
+
"ts":
|
|
395
|
+
hex_zmq_ts_now(),
|
|
396
|
+
"dtype":
|
|
397
|
+
"uint8",
|
|
398
|
+
"shape": (0, ),
|
|
399
|
+
}
|
|
400
|
+
socket.send_multipart(
|
|
401
|
+
[json.dumps(err_hdr).encode("utf-8"),
|
|
402
|
+
memoryview(b"")],
|
|
403
|
+
copy=True)
|
|
404
|
+
|
|
405
|
+
socket.close(0)
|
|
406
|
+
|
|
407
|
+
def start(self):
|
|
408
|
+
for i in range(self._num_workers):
|
|
409
|
+
th = threading.Thread(
|
|
410
|
+
target=self._single_thread,
|
|
411
|
+
args=(i, ),
|
|
412
|
+
daemon=True,
|
|
413
|
+
)
|
|
414
|
+
th.start()
|
|
415
|
+
self._workers.append(th)
|
|
416
|
+
|
|
417
|
+
def _proxy():
|
|
418
|
+
try:
|
|
419
|
+
zmq.proxy(self._frontend, self._backend)
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
self._proxy_thread = threading.Thread(target=_proxy, daemon=True)
|
|
424
|
+
self._proxy_thread.start()
|
|
425
|
+
|
|
426
|
+
def close(self):
|
|
427
|
+
self._stop_event.set()
|
|
428
|
+
try:
|
|
429
|
+
if self._frontend:
|
|
430
|
+
self._frontend.close(0)
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
try:
|
|
434
|
+
if self._backend:
|
|
435
|
+
self._backend.close(0)
|
|
436
|
+
except Exception:
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
def no_ts_hdr(self, hdr: dict, ok_flag: bool) -> dict:
|
|
440
|
+
return {
|
|
441
|
+
"cmd": f"{hdr['cmd']}_ok"
|
|
442
|
+
} if ok_flag else {
|
|
443
|
+
"cmd": f"{hdr['cmd']}_failed"
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
@abstractmethod
|
|
447
|
+
def work_loop(self):
|
|
448
|
+
raise NotImplementedError(
|
|
449
|
+
"`work_loop` should be implemented by the child class")
|
|
450
|
+
|
|
451
|
+
@abstractmethod
|
|
452
|
+
def _process_request(self, recv_hdr: dict, recv_buf: np.ndarray):
|
|
453
|
+
raise NotImplementedError(
|
|
454
|
+
"`_process_request` should be implemented by the child class")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
################################################################
|
|
458
|
+
# Server Helper
|
|
459
|
+
################################################################
|
|
460
|
+
def hex_server_helper(cfg: dict, server_cls: type):
|
|
461
|
+
try:
|
|
462
|
+
net = cfg["net"]
|
|
463
|
+
params = cfg["params"]
|
|
464
|
+
except KeyError as ke:
|
|
465
|
+
missing_key = ke.args[0]
|
|
466
|
+
raise ValueError(f"cfg is not valid, missing key: {missing_key}")
|
|
467
|
+
|
|
468
|
+
server = server_cls(net, params)
|
|
469
|
+
|
|
470
|
+
shutdown_flag = False
|
|
471
|
+
|
|
472
|
+
def signal_handler(signum, frame):
|
|
473
|
+
nonlocal shutdown_flag
|
|
474
|
+
if not shutdown_flag:
|
|
475
|
+
shutdown_flag = True
|
|
476
|
+
print(
|
|
477
|
+
f"[server] Received signal {signal.Signals(signum).name}, shutting down..."
|
|
478
|
+
)
|
|
479
|
+
server._stop_event.set()
|
|
480
|
+
|
|
481
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
482
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
server.start()
|
|
486
|
+
server.work_loop()
|
|
487
|
+
finally:
|
|
488
|
+
server.close()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
################################################################
|
|
492
|
+
# Dummy Sample
|
|
493
|
+
################################################################
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class HexZMQDummyClient(HexZMQClientBase):
|
|
497
|
+
|
|
498
|
+
def __init__(
|
|
499
|
+
self,
|
|
500
|
+
net_config: dict = NET_CONFIG,
|
|
501
|
+
):
|
|
502
|
+
HexZMQClientBase.__init__(self, net_config)
|
|
503
|
+
|
|
504
|
+
def single_test(self):
|
|
505
|
+
resp_hdr, resp_buf = self.request({"cmd": "test"})
|
|
506
|
+
print(f"resp_hdr: {resp_hdr}")
|
|
507
|
+
print(f"resp_buf: {resp_buf}")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class HexZMQDummyServer(HexZMQServerBase):
|
|
511
|
+
|
|
512
|
+
def __init__(
|
|
513
|
+
self,
|
|
514
|
+
net_config: dict = NET_CONFIG,
|
|
515
|
+
params_config: dict = {},
|
|
516
|
+
):
|
|
517
|
+
HexZMQServerBase.__init__(self, net_config)
|
|
518
|
+
|
|
519
|
+
def work_loop(self):
|
|
520
|
+
try:
|
|
521
|
+
while not self._stop_event.is_set():
|
|
522
|
+
time.sleep(1)
|
|
523
|
+
finally:
|
|
524
|
+
self.close()
|
|
525
|
+
|
|
526
|
+
def _process_request(self, recv_hdr: dict, recv_buf: np.ndarray):
|
|
527
|
+
if recv_hdr["cmd"] == "test":
|
|
528
|
+
print("test received")
|
|
529
|
+
print(f"recv_hdr: {recv_hdr}")
|
|
530
|
+
print(f"recv_buf: {recv_buf}")
|
|
531
|
+
resp_hdr = {
|
|
532
|
+
"cmd": "test_ok",
|
|
533
|
+
}
|
|
534
|
+
return resp_hdr, None
|
|
535
|
+
else:
|
|
536
|
+
raise ValueError(f"unknown command: {recv_hdr['cmd']}")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
if __name__ == "__main__":
|
|
540
|
+
import argparse, json
|
|
541
|
+
|
|
542
|
+
parser = argparse.ArgumentParser()
|
|
543
|
+
parser.add_argument("--cfg", type=str, required=True)
|
|
544
|
+
args = parser.parse_args()
|
|
545
|
+
cfg = json.loads(args.cfg)
|
|
546
|
+
|
|
547
|
+
hex_server_helper(cfg, HexZMQDummyServer)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hex_zmq_servers
|
|
3
|
+
Version: 0.3.9
|
|
4
|
+
Summary: HEXFELLOW ZMQ Servers
|
|
5
|
+
Author-email: Dong Zhaorui <joray.dong@hexfellow.com>
|
|
6
|
+
Maintainer-email: jecjune <zejun.chen@hexfellow.com>, Dong Zhaorui <joray.dong@hexfellow.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Project-URL: Homepage, https://github.com/hexfellow/hex_zmq_servers
|
|
9
|
+
Project-URL: Repository, https://github.com/hexfellow/hex_zmq_servers.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/hexfellow/hex_zmq_servers/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/hexfellow/hex_zmq_servers/wiki
|
|
12
|
+
Keywords: hex_zmq_servers
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pyzmq>=27.0.1
|
|
26
|
+
Requires-Dist: hex_device<1.4.0,>=1.3.1
|
|
27
|
+
Requires-Dist: hex_robo_utils<0.3.0,>=0.2.0
|
|
28
|
+
Requires-Dist: dynamixel-sdk==3.8.4
|
|
29
|
+
Requires-Dist: opencv-python>=4.2
|
|
30
|
+
Provides-Extra: berxel
|
|
31
|
+
Requires-Dist: berxel_py_wrapper>=2.0.182; extra == "berxel"
|
|
32
|
+
Provides-Extra: realsense
|
|
33
|
+
Requires-Dist: pyrealsense2>=2.56.5.9235; extra == "realsense"
|
|
34
|
+
Provides-Extra: mujoco
|
|
35
|
+
Requires-Dist: mujoco>=3.3.3; extra == "mujoco"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: berxel_py_wrapper>=2.0.182; extra == "all"
|
|
38
|
+
Requires-Dist: mujoco>=3.3.3; extra == "all"
|
|
39
|
+
Requires-Dist: pyrealsense2>=2.56.5.9235; extra == "all"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# hex_zmq_servers
|
|
43
|
+
|
|
44
|
+
## Introduction
|
|
45
|
+
|
|
46
|
+
**`hex_zmq_servers`** is a comprehensive distributed device control framework based on ZeroMQ, providing efficient client-server communication for HEXFELLOW devices.
|
|
47
|
+
|
|
48
|
+
## Project Structure
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
hex_zmq_servers/
|
|
52
|
+
├── hex_zmq_servers/ # Core library
|
|
53
|
+
│ ├── robot/ # Robot devices
|
|
54
|
+
│ ├── cam/ # Camera devices
|
|
55
|
+
│ ├── mujoco/ # Mujoco simulation devices
|
|
56
|
+
│ └── config/ # Default configuration files
|
|
57
|
+
├── examples/ # Example code
|
|
58
|
+
│ ├── basic/ # Basic examples (single device)
|
|
59
|
+
│ └── adv/ # Advanced examples (multi-device coordination)
|
|
60
|
+
└── venv.sh # Virtual environment script
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Devices
|
|
64
|
+
|
|
65
|
+
### Robot
|
|
66
|
+
|
|
67
|
+
- **dummy**: Dummy robot, for testing and development
|
|
68
|
+
- **gello**: GELLO robot, based on Dynamixel servo
|
|
69
|
+
- **hexarm**: HexArm robot of HEXFELLOW
|
|
70
|
+
|
|
71
|
+
### Camera
|
|
72
|
+
|
|
73
|
+
- **dummy**: Dummy camera, for testing and development
|
|
74
|
+
- **berxel**: Berxel depth camera, providing RGB and depth images
|
|
75
|
+
|
|
76
|
+
### Mujoco
|
|
77
|
+
|
|
78
|
+
- **archer_y6**: Physical simulation of Archer Y6 robot
|
|
79
|
+
- **e3_desktop**: Physical simulation of E3 Desktop robot
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
### Install from PyPI
|
|
84
|
+
|
|
85
|
+
1. For those who only want to use the library in their projects, it is recommended to install it from PyPI.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install hex_zmq_servers[all]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
2. If you don't want to install the extra dependencies for extra devices, you can run:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install hex_zmq_servers
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Install from Source Code
|
|
98
|
+
|
|
99
|
+
1. For those who want to test the examples or contribute to the project, you can install it from source code.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/hexfellow/hex_zmq_servers.git
|
|
103
|
+
cd hex_zmq_servers
|
|
104
|
+
./venv.sh
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
2. If you don't want to install the extra dependencies for extra devices, you can run:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/hexfellow/hex_zmq_servers.git
|
|
111
|
+
cd hex_zmq_servers
|
|
112
|
+
./venv.sh --min
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
(**Important**) Some examples would not work without the extra dependencies.
|
|
116
|
+
|
|
117
|
+
3. If you don't want to install the examples, you can run:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/hexfellow/hex_zmq_servers.git
|
|
121
|
+
cd hex_zmq_servers
|
|
122
|
+
./venv.sh --pkg-only
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Examples
|
|
126
|
+
|
|
127
|
+
There are two types of examples in the project:
|
|
128
|
+
|
|
129
|
+
- **basic/**: Basic examples, showing the usage of a single device
|
|
130
|
+
- **adv/**: Advanced examples, showing multi-device coordination
|
|
131
|
+
|
|
132
|
+
More details please refer to [examples/README.md](examples/README.md)
|
|
133
|
+
|
|
134
|
+
## Contributions
|
|
135
|
+
|
|
136
|
+
Welcome to submit issues and pull requests!
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
Apache License 2.0
|
|
141
|
+
|
|
142
|
+
## Contact
|
|
143
|
+
|
|
144
|
+
- Author: [Dong Zhaorui](https://github.com/IBNBlank)
|
|
145
|
+
- Maintainer: [jecjune](https://github.com/Jecjune)
|
|
146
|
+
- GitHub: [hex_zmq_servers](https://github.com/hexfellow/hex_zmq_servers)
|
|
147
|
+
- Issue Tracker: [hex_zmq_servers](https://github.com/hexfellow/hex_zmq_servers/issues)
|