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,44 @@
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-16
7
+ ################################################################
8
+
9
+ import threading
10
+ from collections import deque
11
+ from abc import ABC, abstractmethod
12
+
13
+ from .zmq_base import MAX_SEQ_NUM
14
+
15
+
16
+ class HexDeviceBase(ABC):
17
+
18
+ def __init__(self, realtime_mode: bool = False):
19
+ # variables
20
+ self._max_seq_num = MAX_SEQ_NUM
21
+ self._realtime_mode = realtime_mode
22
+ # thread
23
+ self._working = threading.Event()
24
+
25
+ def __del__(self):
26
+ self.close()
27
+
28
+ def is_working(self) -> bool:
29
+ return self._working.is_set()
30
+
31
+ def _wait_for_working(self):
32
+ while not self._working.is_set():
33
+ print("waiting for device to work")
34
+ self._working.wait(0.1)
35
+
36
+ @abstractmethod
37
+ def work_loop(self, hex_queues: list[deque | threading.Event]):
38
+ raise NotImplementedError(
39
+ "`work_loop` should be implemented by the child class")
40
+
41
+ @abstractmethod
42
+ def close(self):
43
+ raise NotImplementedError(
44
+ "`close` should be implemented by the child class")
@@ -0,0 +1,489 @@
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-25
7
+ ################################################################
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os, sys, signal, subprocess, threading, time
13
+ import termios
14
+ import importlib.util
15
+
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+
19
+ HEX_LOG_LEVEL = {
20
+ "info": 0,
21
+ "warn": 1,
22
+ "err": 2,
23
+ }
24
+
25
+
26
+ def hex_log(level: int, message):
27
+ print(f"[{level}] {message}")
28
+
29
+
30
+ def hex_err(message):
31
+ print(message, file=sys.stderr)
32
+
33
+
34
+ def dict_update(dict_raw: dict, dict_new: dict, add_new: bool = False):
35
+ for key, value in dict_new.items():
36
+ if key in dict_raw:
37
+ if isinstance(dict_raw[key], dict) and isinstance(value, dict):
38
+ dict_update(dict_raw[key], value)
39
+ else:
40
+ dict_raw[key] = value
41
+ elif add_new:
42
+ dict_raw[key] = value
43
+
44
+
45
+ def hex_dict_str(dict_raw: dict, indent: int = 0) -> str:
46
+ print_str = ("\n" + "*" * 50 + "\n") if indent == 0 else ""
47
+ for key, value in dict_raw.items():
48
+ if isinstance(value, dict):
49
+ print_str += f"{' ' * indent * 4}{key}:\n{hex_dict_str(value, indent + 1)}"
50
+ else:
51
+ print_str += f"{' ' * indent * 4}{key}: {value}\n"
52
+ print_str += ("*" * 50) if indent == 0 else ""
53
+ return print_str
54
+
55
+
56
+ class HexNodeConfig():
57
+
58
+ def __init__(
59
+ self,
60
+ init_params: dict[str, dict] | list[dict] | HexNodeConfig = {},
61
+ ):
62
+ len_init_params = len(init_params)
63
+ if isinstance(init_params, list):
64
+ self._cfgs_dict = {cfg["name"]: cfg for cfg in init_params}
65
+ elif isinstance(init_params, dict):
66
+ self._cfgs_dict = init_params
67
+ if len_init_params != len(self._cfgs_dict):
68
+ raise ValueError(f"Invalid init_params: {init_params}")
69
+ elif isinstance(init_params, HexNodeConfig):
70
+ self._cfgs_dict = init_params._cfgs_dict
71
+ else:
72
+ raise ValueError(f"Invalid init_params: {init_params}")
73
+
74
+ assert len(self._cfgs_dict
75
+ ) == len_init_params, f"Invalid init_params: {init_params}"
76
+
77
+ def __len__(self) -> int:
78
+ return len(self._cfgs_dict)
79
+
80
+ def get_cfgs(self, use_list: bool = True) -> list[dict]:
81
+ if use_list:
82
+ return list(self._cfgs_dict.values())
83
+ else:
84
+ return self._cfgs_dict
85
+
86
+ def add_cfgs(
87
+ self,
88
+ node_cfgs: list[dict] | dict[str, dict] | HexNodeConfig,
89
+ ):
90
+ new_cfgs = {}
91
+ if isinstance(node_cfgs, list):
92
+ new_cfgs = {cfg["name"]: cfg for cfg in node_cfgs}
93
+ elif isinstance(node_cfgs, dict):
94
+ new_cfgs = node_cfgs
95
+ elif isinstance(node_cfgs, HexNodeConfig):
96
+ new_cfgs = node_cfgs.get_cfgs(use_list=False)
97
+ else:
98
+ raise ValueError(f"Invalid node_cfgs: {node_cfgs}")
99
+
100
+ dict_update(self._cfgs_dict, new_cfgs, add_new=True)
101
+
102
+ def __str__(self) -> str:
103
+ print_str = f"[HexNodeConfig] Total {len(self._cfgs_dict)} nodes:\n"
104
+ for name in self._cfgs_dict.keys():
105
+ print_str += f" - {name}\n"
106
+ return print_str
107
+
108
+ @staticmethod
109
+ def parse_node_params_dict(
110
+ node_params_dict: dict,
111
+ node_default_params_dict: dict,
112
+ ) -> HexNodeConfig:
113
+ node_dict = {}
114
+ for cur_name, cur_default_params in node_default_params_dict.items():
115
+ if cur_name in node_params_dict.keys():
116
+ dict_update(cur_default_params, node_params_dict[cur_name])
117
+ node_dict[cur_name] = cur_default_params
118
+ return HexNodeConfig(node_dict)
119
+
120
+ @staticmethod
121
+ def get_launch_params_cfgs(
122
+ launch_params_dict: dict,
123
+ launch_default_params_dict: dict,
124
+ launch_path_dict: dict,
125
+ ) -> HexNodeConfig:
126
+ cfg_list = []
127
+ for launch_name, (launch_path, launch_arg) in launch_path_dict.items():
128
+ node_default_params_dict = launch_default_params_dict.get(
129
+ launch_name, {})
130
+
131
+ node_params_dict = {}
132
+ if launch_name in launch_params_dict.keys():
133
+ node_params_dict = launch_params_dict[launch_name]
134
+ else:
135
+ for node_name in node_default_params_dict.keys():
136
+ if node_name in launch_params_dict.keys():
137
+ node_params_dict[node_name] = launch_params_dict[
138
+ node_name]
139
+
140
+ launch_update_cfg = HexNodeConfig.parse_node_params_dict(
141
+ node_params_dict,
142
+ node_default_params_dict,
143
+ )
144
+
145
+ cfg_list.append(
146
+ HexNodeConfig.get_node_cfgs_from_launch(
147
+ launch_path,
148
+ launch_update_cfg,
149
+ launch_arg,
150
+ ))
151
+
152
+ final_cfg = HexNodeConfig()
153
+ for cfg in cfg_list:
154
+ # use name as key to make sure every node has a unique name
155
+ final_cfg.add_cfgs(cfg.get_cfgs(use_list=True))
156
+ print(f"final_cfg: {final_cfg}")
157
+ return final_cfg
158
+
159
+ @staticmethod
160
+ def get_node_cfgs_from_launch(
161
+ launch_path: str,
162
+ params: dict | HexNodeConfig = {},
163
+ launch_arg: dict | None = None,
164
+ ) -> HexNodeConfig:
165
+ # normalize the path
166
+ launch_path = os.path.abspath(launch_path)
167
+ if not os.path.exists(launch_path):
168
+ raise FileNotFoundError(f"Launch file not found: {launch_path}")
169
+
170
+ # load the module dynamically
171
+ spec = importlib.util.spec_from_file_location("launch_module",
172
+ launch_path)
173
+ if spec is None or spec.loader is None:
174
+ raise ImportError(f"Failed to load module from: {launch_path}")
175
+
176
+ # load module
177
+ launch_module = importlib.util.module_from_spec(spec)
178
+ sys.modules["launch_module"] = launch_module
179
+ spec.loader.exec_module(launch_module)
180
+
181
+ # check if `get_node_cfgs` function exists
182
+ if not hasattr(launch_module, "get_node_cfgs"):
183
+ raise AttributeError(
184
+ f"Function 'get_node_cfgs' not found in {launch_path}")
185
+
186
+ # call `get_node_cfgs` function
187
+ get_node_cfgs_func = getattr(launch_module, "get_node_cfgs")
188
+ if isinstance(params, dict):
189
+ print("params is dict")
190
+ node_cfgs = get_node_cfgs_func(params, launch_arg)
191
+ elif isinstance(params, HexNodeConfig):
192
+ print("params is HexNodeConfig")
193
+ node_cfgs = get_node_cfgs_func(
194
+ params.get_cfgs(use_list=False),
195
+ launch_arg,
196
+ )
197
+ else:
198
+ raise ValueError(f"Invalid params: {params}")
199
+ return node_cfgs
200
+
201
+
202
+ class HexLaunch:
203
+
204
+ def __init__(
205
+ self,
206
+ node_cfgs: list[dict] | dict[str, dict] | HexNodeConfig,
207
+ log_dir: str = "logs",
208
+ min_level: int = HEX_LOG_LEVEL["warn"],
209
+ ):
210
+ if isinstance(node_cfgs, list):
211
+ node_cfgs = HexNodeConfig(node_cfgs)
212
+ elif isinstance(node_cfgs, dict):
213
+ node_cfgs = HexNodeConfig(node_cfgs)
214
+ elif isinstance(node_cfgs, HexNodeConfig):
215
+ node_cfgs = node_cfgs
216
+ else:
217
+ raise ValueError(f"Invalid node_cfgs: {node_cfgs}")
218
+ self.__node_cfgs = node_cfgs.get_cfgs(use_list=True)
219
+ self.__state: dict[str, dict] = {}
220
+ self.__stop_event = threading.Event()
221
+ self.__log_dir = Path(log_dir)
222
+ self.__log_dir.mkdir(parents=True, exist_ok=True)
223
+ self.__min_level = min_level
224
+ # lock state for node messages
225
+ self.__state_lock = threading.Lock()
226
+ self.__shutdown_called = False
227
+ # terminal attrs
228
+ self.__terminal_attrs = None
229
+ self.__record_terminal_attrs()
230
+
231
+ def __record_terminal_attrs(self):
232
+ """record terminal attrs"""
233
+ try:
234
+ self.__terminal_attrs = termios.tcgetattr(sys.stdin.fileno())
235
+ self.__terminal_attrs[3] |= (termios.ICANON | termios.ECHO)
236
+ try:
237
+ self.__terminal_attrs[6][termios.VMIN] = 1
238
+ self.__terminal_attrs[6][termios.VTIME] = 0
239
+ except Exception:
240
+ pass
241
+ print("[launcher] Terminal settings recorded")
242
+ except Exception as e:
243
+ print(f"[launcher] Failed to record terminal attrs: {e}")
244
+
245
+ def __restore_terminal_attrs(self):
246
+ """restore terminal attrs"""
247
+ if self.__terminal_attrs is not None:
248
+ try:
249
+ termios.tcsetattr(
250
+ sys.stdin.fileno(),
251
+ termios.TCSADRAIN,
252
+ self.__terminal_attrs,
253
+ )
254
+ print("[launcher] Terminal attrs restored")
255
+ except Exception as e:
256
+ print(f"[launcher] Failed to restore terminal attrs: {e}")
257
+ else:
258
+ print("[launcher] No terminal attrs to restore")
259
+
260
+ def __build_cmd(self, node_cfg: dict):
261
+ # python path
262
+ venv = node_cfg.get("venv") or None
263
+ python_exe = os.path.join(
264
+ venv,
265
+ "bin",
266
+ "python",
267
+ ) if venv else sys.executable
268
+
269
+ # node path
270
+ node_path = node_cfg.get("node_path") or None
271
+ if not node_path:
272
+ raise ValueError("node_path is required")
273
+
274
+ # cfg
275
+ cfg_obj = {"name": node_cfg.get("name")}
276
+ cfg_path = node_cfg.get("cfg_path", "") or None
277
+ if cfg_path:
278
+ if not os.path.exists(cfg_path):
279
+ raise FileNotFoundError(f"cfg_path {cfg_path} not found")
280
+ with open(cfg_path, "r", encoding="utf-8") as f:
281
+ cfg_file = json.load(f)
282
+ cfg_obj.update(cfg_file)
283
+ dict_update(cfg_obj, node_cfg.get("cfg", {}))
284
+ cfg_str = json.dumps(cfg_obj)
285
+
286
+ # build cmd
287
+ return [python_exe, "-u", str(node_path)] + ["--cfg", cfg_str]
288
+
289
+ def __stream_printer(
290
+ self,
291
+ prefix: str,
292
+ stream,
293
+ is_stderr: bool,
294
+ logfile_path: Path | None,
295
+ min_level: int,
296
+ ):
297
+ logfile = logfile_path.open("a",
298
+ encoding="utf-8") if logfile_path else None
299
+ try:
300
+ while True:
301
+ raw = stream.readline()
302
+ if not raw:
303
+ time.sleep(0.001)
304
+ continue
305
+ line = raw.decode(errors="replace").rstrip("\n")
306
+
307
+ record_flag = True
308
+ print_start, print_end, log_start, log_end = "", "", "", ""
309
+ if not is_stderr:
310
+ # parse level if present
311
+ level = HEX_LOG_LEVEL["info"]
312
+ if line.startswith("[") and "]" in line:
313
+ try:
314
+ lvl = int(line[1:line.index("]")])
315
+ if lvl in HEX_LOG_LEVEL.values():
316
+ level = lvl
317
+ line = line[line.index("]") + 1:].lstrip()
318
+ except Exception:
319
+ level = HEX_LOG_LEVEL["info"]
320
+
321
+ if level < min_level:
322
+ record_flag = False
323
+
324
+ # colored print
325
+ if level == HEX_LOG_LEVEL["info"]:
326
+ color_start, color_end = "", ""
327
+ level_str = "info"
328
+ elif level == HEX_LOG_LEVEL["warn"]:
329
+ color_start, color_end = "\033[33m", "\033[0m"
330
+ level_str = "warn"
331
+ elif level == HEX_LOG_LEVEL["err"]:
332
+ color_start, color_end = "\033[31m", "\033[0m"
333
+ level_str = "err"
334
+
335
+ print_start, print_end = f"{color_start}[{prefix}] ", f"{color_end}"
336
+ log_start, log_end = f"[{level_str}] ", ""
337
+ else:
338
+ print_start, print_end = f"\033[31m[FATAL][{prefix}] ", "\033[0m"
339
+ log_start, log_end = "", ""
340
+
341
+ print(f"{print_start}{line}{print_end}", flush=True)
342
+ if logfile and record_flag:
343
+ logfile.write(f"{log_start}{line}{log_end}\n")
344
+ logfile.flush()
345
+ finally:
346
+ if logfile:
347
+ logfile.close()
348
+
349
+ def __start_node(self, node_cfg: dict):
350
+ name = node_cfg.get("name", node_cfg.get("node_path"))
351
+ env = os.environ.copy()
352
+ env.update(node_cfg.get("env", {}) or {})
353
+ cwd = node_cfg.get("cwd") or None
354
+ cmd = self.__build_cmd(node_cfg)
355
+
356
+ proc = subprocess.Popen(
357
+ cmd,
358
+ cwd=cwd,
359
+ env=env,
360
+ stdout=subprocess.PIPE,
361
+ stderr=subprocess.PIPE,
362
+ preexec_fn=os.setsid,
363
+ )
364
+
365
+ log_path = Path(
366
+ f"{self.__log_dir}/info/{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
367
+ )
368
+ err_path = Path(
369
+ f"{self.__log_dir}/err/{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
370
+ )
371
+ log_path.parent.mkdir(parents=True, exist_ok=True)
372
+ err_path.parent.mkdir(parents=True, exist_ok=True)
373
+
374
+ log_thread = threading.Thread(target=self.__stream_printer,
375
+ args=(name, proc.stdout, False, log_path,
376
+ self.__min_level),
377
+ daemon=True)
378
+ err_thread = threading.Thread(target=self.__stream_printer,
379
+ args=(name, proc.stderr, True, err_path,
380
+ self.__min_level),
381
+ daemon=True)
382
+ log_thread.start()
383
+ err_thread.start()
384
+
385
+ return {
386
+ "proc": proc,
387
+ "threads": (log_thread, err_thread),
388
+ "cfg": node_cfg
389
+ }
390
+
391
+ def __monitor_loop(self):
392
+ print(f"[launcher] Started {len(self.__node_cfgs)} nodes")
393
+ for node in self.__node_cfgs:
394
+ print(
395
+ f"[launcher] Starting {node.get('name', node.get('node_path'))}"
396
+ )
397
+ entry = self.__start_node(node)
398
+ with self.__state_lock:
399
+ self.__state[node.get("name", node.get("node_path"))] = entry
400
+
401
+ try:
402
+ while not self.__stop_event.is_set():
403
+ with self.__state_lock:
404
+ if not self.__state:
405
+ break
406
+ state_items = list(self.__state.items())
407
+
408
+ for name, entry in state_items:
409
+ proc = entry["proc"]
410
+ cfg_node = entry["cfg"]
411
+
412
+ if proc.poll() is not None:
413
+ print(
414
+ f"[launcher] Node {name} exited {proc.returncode}")
415
+ # Threads will stop automatically when stream ends
416
+
417
+ if cfg_node.get("respawn"):
418
+ delay = cfg_node.get("respawn_delay", 1)
419
+ print(
420
+ f"[launcher] Respawning {name} in {delay}s...")
421
+ time.sleep(delay)
422
+ new_entry = self.__start_node(cfg_node)
423
+ with self.__state_lock:
424
+ self.__state[name] = new_entry
425
+ else:
426
+ with self.__state_lock:
427
+ del self.__state[name]
428
+
429
+ time.sleep(0.5)
430
+ except KeyboardInterrupt:
431
+ pass
432
+
433
+ def __shutdown(self, signame):
434
+ if self.__shutdown_called:
435
+ return
436
+ self.__shutdown_called = True
437
+
438
+ print(f"[launcher] Got signal {signame}, shutting down children...")
439
+ with self.__state_lock:
440
+ state_items = list(self.__state.items())
441
+
442
+ for _, entry in state_items:
443
+ proc = entry["proc"]
444
+ try:
445
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
446
+ except Exception:
447
+ try:
448
+ proc.terminate()
449
+ except Exception:
450
+ pass
451
+ self.__stop_event.set()
452
+ self.__restore_terminal_attrs()
453
+
454
+ def run(self):
455
+ # Set up signal handlers
456
+ for s in (signal.SIGINT, signal.SIGTERM):
457
+ signal.signal(
458
+ s, lambda signum, frame: self.__shutdown(
459
+ signal.Signals(signum).name))
460
+
461
+ # Start monitor in a separate thread
462
+ monitor_thread = threading.Thread(target=self.__monitor_loop,
463
+ daemon=True)
464
+ monitor_thread.start()
465
+
466
+ # Wait for stop event or monitor thread to finish
467
+ while monitor_thread.is_alive() and not self.__stop_event.is_set():
468
+ self.__stop_event.wait(timeout=0.5)
469
+
470
+ # Give processes time to terminate gracefully
471
+ time.sleep(1.0)
472
+
473
+ # Force kill any remaining processes
474
+ with self.__state_lock:
475
+ state_items = list(self.__state.items())
476
+
477
+ for name, entry in state_items:
478
+ proc = entry["proc"]
479
+ if proc.poll() is None:
480
+ try:
481
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
482
+ except Exception:
483
+ try:
484
+ proc.kill()
485
+ except Exception:
486
+ pass
487
+
488
+ # Wait for monitor thread to finish
489
+ monitor_thread.join(timeout=0.5)
@@ -0,0 +1,28 @@
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-15
7
+ ################################################################
8
+
9
+ from .mujoco_base import HexMujocoBase, HexMujocoClientBase, HexMujocoServerBase
10
+ from .archer_y6 import HexMujocoArcherY6, HexMujocoArcherY6Client, HexMujocoArcherY6Server
11
+ from .e3_desktop import HexMujocoE3Desktop, HexMujocoE3DesktopClient, HexMujocoE3DesktopServer
12
+
13
+ __all__ = [
14
+ # base
15
+ "HexMujocoBase",
16
+ "HexMujocoClientBase",
17
+ "HexMujocoServerBase",
18
+
19
+ # archer_y6
20
+ "HexMujocoArcherY6",
21
+ "HexMujocoArcherY6Client",
22
+ "HexMujocoArcherY6Server",
23
+
24
+ # e3_desktop
25
+ "HexMujocoE3Desktop",
26
+ "HexMujocoE3DesktopClient",
27
+ "HexMujocoE3DesktopServer",
28
+ ]
@@ -0,0 +1,17 @@
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-15
7
+ ################################################################
8
+
9
+ from .mujoco_archer_y6 import HexMujocoArcherY6
10
+ from .mujoco_archer_y6_cli import HexMujocoArcherY6Client
11
+ from .mujoco_archer_y6_srv import HexMujocoArcherY6Server
12
+
13
+ __all__ = [
14
+ "HexMujocoArcherY6",
15
+ "HexMujocoArcherY6Client",
16
+ "HexMujocoArcherY6Server",
17
+ ]
@@ -0,0 +1,17 @@
1
+ <mujocoinclude>
2
+ <mesh name="arm_base_link" file="arm_base_link.STL"/>
3
+ <mesh name="arm_link_1" file="arm_link_1.STL"/>
4
+ <mesh name="arm_link_2" file="arm_link_2.STL"/>
5
+ <mesh name="arm_link_3" file="arm_link_3.STL"/>
6
+ <mesh name="arm_link_4" file="arm_link_4.STL"/>
7
+ <mesh name="arm_link_5" file="arm_link_5.STL"/>
8
+ <mesh name="gripper_base_link" file="gripper_base_link.STL"/>
9
+ <mesh name="gripper_left_helper_link" file="gripper_left_helper_link.STL"/>
10
+ <mesh name="gripper_left_link_1" file="gripper_left_link_1.STL"/>
11
+ <mesh name="gripper_left_link_2" file="gripper_left_link_2.STL"/>
12
+ <mesh name="gripper_right_helper_link" file="gripper_right_helper_link.STL"/>
13
+ <mesh name="gripper_right_link_1" file="gripper_right_link_1.STL"/>
14
+ <mesh name="gripper_right_link_2" file="gripper_right_link_2.STL"/>
15
+ <mesh name="camera_link" file="camera_link.STL"/>
16
+ <mesh name="table_link" file="table_link.STL" scale="0.001 0.001 0.001"/>
17
+ </mujocoinclude>