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.
Files changed (110) hide show
  1. hex_zmq_servers/__init__.py +173 -0
  2. hex_zmq_servers/cam/__init__.py +52 -0
  3. hex_zmq_servers/cam/berxel/__init__.py +17 -0
  4. hex_zmq_servers/cam/berxel/cam_berxel.py +282 -0
  5. hex_zmq_servers/cam/berxel/cam_berxel_cli.py +33 -0
  6. hex_zmq_servers/cam/berxel/cam_berxel_srv.py +79 -0
  7. hex_zmq_servers/cam/cam_base.py +189 -0
  8. hex_zmq_servers/cam/dummy/__init__.py +17 -0
  9. hex_zmq_servers/cam/dummy/cam_dummy.py +69 -0
  10. hex_zmq_servers/cam/dummy/cam_dummy_cli.py +29 -0
  11. hex_zmq_servers/cam/dummy/cam_dummy_srv.py +68 -0
  12. hex_zmq_servers/cam/realsense/__init__.py +17 -0
  13. hex_zmq_servers/cam/realsense/cam_realsense.py +159 -0
  14. hex_zmq_servers/cam/realsense/cam_realsense_cli.py +33 -0
  15. hex_zmq_servers/cam/realsense/cam_realsense_srv.py +78 -0
  16. hex_zmq_servers/cam/rgb/__init__.py +17 -0
  17. hex_zmq_servers/cam/rgb/cam_rgb.py +135 -0
  18. hex_zmq_servers/cam/rgb/cam_rgb_cli.py +43 -0
  19. hex_zmq_servers/cam/rgb/cam_rgb_srv.py +78 -0
  20. hex_zmq_servers/config/cam_berxel.json +18 -0
  21. hex_zmq_servers/config/cam_dummy.json +12 -0
  22. hex_zmq_servers/config/cam_realsense.json +17 -0
  23. hex_zmq_servers/config/cam_rgb.json +28 -0
  24. hex_zmq_servers/config/mujoco_archer_y6.json +37 -0
  25. hex_zmq_servers/config/mujoco_e3_desktop.json +41 -0
  26. hex_zmq_servers/config/robot_dummy.json +153 -0
  27. hex_zmq_servers/config/robot_gello.json +66 -0
  28. hex_zmq_servers/config/robot_hexarm.json +37 -0
  29. hex_zmq_servers/config/zmq_dummy.json +12 -0
  30. hex_zmq_servers/device_base.py +44 -0
  31. hex_zmq_servers/hex_launch.py +489 -0
  32. hex_zmq_servers/mujoco/__init__.py +28 -0
  33. hex_zmq_servers/mujoco/archer_y6/__init__.py +17 -0
  34. hex_zmq_servers/mujoco/archer_y6/model/assets/arm_base_link.STL +0 -0
  35. hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_1.STL +0 -0
  36. hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_2.STL +0 -0
  37. hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_3.STL +0 -0
  38. hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_4.STL +0 -0
  39. hex_zmq_servers/mujoco/archer_y6/model/assets/arm_link_5.STL +0 -0
  40. hex_zmq_servers/mujoco/archer_y6/model/assets/assets.xml +17 -0
  41. hex_zmq_servers/mujoco/archer_y6/model/assets/camera_link.STL +0 -0
  42. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_base_link.STL +0 -0
  43. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_left_helper_link.STL +0 -0
  44. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_left_link_1.STL +0 -0
  45. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_left_link_2.STL +0 -0
  46. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_right_helper_link.STL +0 -0
  47. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_right_link_1.STL +0 -0
  48. hex_zmq_servers/mujoco/archer_y6/model/assets/gripper_right_link_2.STL +0 -0
  49. hex_zmq_servers/mujoco/archer_y6/model/assets/table_link.STL +0 -0
  50. hex_zmq_servers/mujoco/archer_y6/model/robot.xml +95 -0
  51. hex_zmq_servers/mujoco/archer_y6/model/scene.xml +51 -0
  52. hex_zmq_servers/mujoco/archer_y6/model/setting.xml +37 -0
  53. hex_zmq_servers/mujoco/archer_y6/mujoco_archer_y6.py +325 -0
  54. hex_zmq_servers/mujoco/archer_y6/mujoco_archer_y6_cli.py +71 -0
  55. hex_zmq_servers/mujoco/archer_y6/mujoco_archer_y6_srv.py +148 -0
  56. hex_zmq_servers/mujoco/e3_desktop/__init__.py +17 -0
  57. hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_base_link.STL +0 -0
  58. hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_1.STL +0 -0
  59. hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_2.STL +0 -0
  60. hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_3.STL +0 -0
  61. hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_4.STL +0 -0
  62. hex_zmq_servers/mujoco/e3_desktop/model/assets/arm_link_5.STL +0 -0
  63. hex_zmq_servers/mujoco/e3_desktop/model/assets/assets.xml +18 -0
  64. hex_zmq_servers/mujoco/e3_desktop/model/assets/camera_link.STL +0 -0
  65. hex_zmq_servers/mujoco/e3_desktop/model/assets/e3_desktop_base_link.STL +0 -0
  66. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_base_link.STL +0 -0
  67. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_left_helper_link.STL +0 -0
  68. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_left_link_1.STL +0 -0
  69. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_left_link_2.STL +0 -0
  70. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_right_helper_link.STL +0 -0
  71. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_right_link_1.STL +0 -0
  72. hex_zmq_servers/mujoco/e3_desktop/model/assets/gripper_right_link_2.STL +0 -0
  73. hex_zmq_servers/mujoco/e3_desktop/model/assets/table_link.STL +0 -0
  74. hex_zmq_servers/mujoco/e3_desktop/model/robot.xml +188 -0
  75. hex_zmq_servers/mujoco/e3_desktop/model/scene.xml +53 -0
  76. hex_zmq_servers/mujoco/e3_desktop/model/setting.xml +72 -0
  77. hex_zmq_servers/mujoco/e3_desktop/mujoco_e3_desktop.py +449 -0
  78. hex_zmq_servers/mujoco/e3_desktop/mujoco_e3_desktop_cli.py +289 -0
  79. hex_zmq_servers/mujoco/e3_desktop/mujoco_e3_desktop_srv.py +244 -0
  80. hex_zmq_servers/mujoco/mujoco_base.py +425 -0
  81. hex_zmq_servers/robot/__init__.py +37 -0
  82. hex_zmq_servers/robot/dummy/__init__.py +17 -0
  83. hex_zmq_servers/robot/dummy/robot_dummy.py +94 -0
  84. hex_zmq_servers/robot/dummy/robot_dummy_cli.py +29 -0
  85. hex_zmq_servers/robot/dummy/robot_dummy_srv.py +82 -0
  86. hex_zmq_servers/robot/gello/__init__.py +17 -0
  87. hex_zmq_servers/robot/gello/robot_gello.py +366 -0
  88. hex_zmq_servers/robot/gello/robot_gello_cli.py +29 -0
  89. hex_zmq_servers/robot/gello/robot_gello_srv.py +93 -0
  90. hex_zmq_servers/robot/hexarm/__init__.py +47 -0
  91. hex_zmq_servers/robot/hexarm/robot_hexarm.py +292 -0
  92. hex_zmq_servers/robot/hexarm/robot_hexarm_cli.py +37 -0
  93. hex_zmq_servers/robot/hexarm/robot_hexarm_srv.py +87 -0
  94. hex_zmq_servers/robot/hexarm/urdf/archer_l6y/empty.urdf +206 -0
  95. hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100.urdf +206 -0
  96. hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100_handle.urdf +206 -0
  97. hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100_p050.urdf +206 -0
  98. hex_zmq_servers/robot/hexarm/urdf/archer_l6y/gp100_p050_handle.urdf +206 -0
  99. hex_zmq_servers/robot/hexarm/urdf/archer_y6/empty.urdf +207 -0
  100. hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100.urdf +207 -0
  101. hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100_handle.urdf +207 -0
  102. hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100_p050.urdf +207 -0
  103. hex_zmq_servers/robot/hexarm/urdf/archer_y6/gp100_p050_handle.urdf +207 -0
  104. hex_zmq_servers/robot/robot_base.py +276 -0
  105. hex_zmq_servers/zmq_base.py +547 -0
  106. hex_zmq_servers-0.3.9.dist-info/METADATA +147 -0
  107. hex_zmq_servers-0.3.9.dist-info/RECORD +110 -0
  108. hex_zmq_servers-0.3.9.dist-info/WHEEL +5 -0
  109. hex_zmq_servers-0.3.9.dist-info/licenses/LICENSE +201 -0
  110. 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)