superbot2 0.1.0__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.
superbot/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ SuperBot - A modular robotics framework
3
+ """
4
+
5
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,103 @@
1
+ """
2
+ Base robot class with shared control API
3
+ All specific robot implementations should inherit from this class
4
+ """
5
+
6
+ import threading
7
+ import time
8
+ from abc import ABC, abstractmethod
9
+ from typing import Optional
10
+ import numpy as np
11
+ from scipy.spatial.transform import Rotation as R
12
+ from loguru import logger
13
+
14
+
15
+ class BaseRobot(ABC):
16
+ """
17
+ Abstract base class for all robots with shared control API.
18
+ Each specific robot implementation should inherit from this class.
19
+ """
20
+
21
+ def __init__(self):
22
+ self._state_cache = {}
23
+ self._running = False
24
+ self.connected = False
25
+ self._state_thread = None
26
+
27
+ @abstractmethod
28
+ def connect(self):
29
+ """Connect to the robot hardware."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def disconnect(self):
34
+ """Disconnect from the robot hardware."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def _state_update_loop(self):
39
+ """Continuously update the robot state in a background thread."""
40
+ pass
41
+
42
+ def start_state_monitoring(self):
43
+ """Start the background thread for state monitoring."""
44
+ if self._state_thread is None or not self._state_thread.is_alive():
45
+ self._running = True
46
+ self._state_thread = threading.Thread(
47
+ target=self._state_update_loop, daemon=True
48
+ )
49
+ self._state_thread.start()
50
+
51
+ def stop_state_monitoring(self):
52
+ """Stop the background thread for state monitoring."""
53
+ self._running = False
54
+ if self._state_thread:
55
+ self._state_thread.join(
56
+ timeout=1.0
57
+ ) # Wait up to 1 second for thread to finish
58
+
59
+ @abstractmethod
60
+ def set_gripper(
61
+ self,
62
+ left_value: float,
63
+ right_value: float,
64
+ speed: int = 1000,
65
+ force: int = 0,
66
+ ):
67
+ """Control the grippers of the robot."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def move_to_joint(self, action_joint):
72
+ """Move the robot to specific joint positions."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ def move_to_pose(
77
+ self,
78
+ left_target: Optional[np.ndarray] = None,
79
+ right_target: Optional[np.ndarray] = None,
80
+ duration: float = 1,
81
+ ):
82
+ """Move the robot to specific end-effector poses."""
83
+ pass
84
+
85
+ @abstractmethod
86
+ def go_home(self):
87
+ """Move the robot to its home position."""
88
+ pass
89
+
90
+ @abstractmethod
91
+ def safe_stop(self):
92
+ """Safely stop the robot motion."""
93
+ pass
94
+
95
+ @abstractmethod
96
+ def get_state_pos(self):
97
+ """Get the current joint positions."""
98
+ pass
99
+
100
+ @abstractmethod
101
+ def get_current_state(self):
102
+ """Get the complete current state of the robot."""
103
+ pass
@@ -0,0 +1,360 @@
1
+ """
2
+ Dual Piper robot implementation.
3
+ This module implements the Dual Piper robot using the Piper SDK.
4
+ """
5
+
6
+ import time
7
+ from typing import Optional
8
+ import numpy as np
9
+ from scipy.spatial.transform import Rotation as R
10
+ from loguru import logger
11
+
12
+ from .base_bot import BaseRobot
13
+
14
+
15
+ class DualPiperRobot(BaseRobot):
16
+ """
17
+ Dual Piper Robot implementation using Piper SDK.
18
+ Implements the shared control API from BaseRobot.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ can_interfaces=("can_left", "can_right"),
24
+ ):
25
+ super().__init__()
26
+
27
+ # Attempt to import the Piper SDK
28
+ try:
29
+ from piper_sdk import C_PiperInterface_V2
30
+
31
+ self.piper_sdk_available = True
32
+ except ImportError:
33
+ logger.warning(
34
+ "Piper SDK not found. Please install piper_sdk for full functionality."
35
+ )
36
+ logger.warning("Using mock implementation for demonstration.")
37
+ # Use mock implementation
38
+ from .dual_piper_mock import C_PiperInterface_V2
39
+
40
+ self.piper_sdk_available = False
41
+
42
+ self.left_arm = None
43
+ self.right_arm = None
44
+ self.can_interfaces = can_interfaces
45
+
46
+ for can_if in can_interfaces:
47
+ arm = C_PiperInterface_V2(can_if)
48
+ arm.ConnectPort()
49
+ while not arm.EnablePiper():
50
+ time.sleep(0.01)
51
+
52
+ if "left" in can_if:
53
+ self.left_arm = arm
54
+ logger.info("Left arm connected on CAN interface: {}".format(can_if))
55
+ elif "right" in can_if:
56
+ self.right_arm = arm
57
+ logger.info("Right arm connected on CAN interface: {}".format(can_if))
58
+
59
+ if self.left_arm is None or self.right_arm is None:
60
+ raise RuntimeError("左右臂 CAN 接口未正确配置")
61
+
62
+ self._state_cache = {
63
+ "left_arm": {"qpos": [0.0] * 6},
64
+ "right_arm": {"qpos": [0.0] * 6},
65
+ "left_joint": {"qpos": [0.0] * 6},
66
+ "right_joint": {"qpos": [0.0] * 6},
67
+ "left_ctrl": {"qpos": [0.0] * 6},
68
+ "right_ctrl": {"qpos": [0.0] * 6},
69
+ "left_vr_pose": {"qpos": [0.0] * 7},
70
+ "right_vr_pose": {"qpos": [0.0] * 7},
71
+ "left_gripper": [0.0],
72
+ "right_gripper": [0.0],
73
+ }
74
+
75
+ self.connected = True
76
+ self.set_gripper(0.0, 0.0)
77
+ time.sleep(0.5)
78
+
79
+ def connect(self):
80
+ """Connect to the Dual Piper robot hardware."""
81
+ if not self.connected:
82
+ for can_if in self.can_interfaces:
83
+ arm = C_PiperInterface_V2(can_if)
84
+ arm.ConnectPort()
85
+ while not arm.EnablePiper():
86
+ time.sleep(0.01)
87
+
88
+ if "left" in can_if:
89
+ self.left_arm = arm
90
+ logger.info(
91
+ "Left arm connected on CAN interface: {}".format(can_if)
92
+ )
93
+ elif "right" in can_if:
94
+ self.right_arm = arm
95
+ logger.info(
96
+ "Right arm connected on CAN interface: {}".format(can_if)
97
+ )
98
+
99
+ self.connected = True
100
+ self.start_state_monitoring()
101
+ return True
102
+
103
+ def disconnect(self):
104
+ """Disconnect from the Dual Piper robot hardware."""
105
+ self._running = False
106
+ self.safe_stop()
107
+ time.sleep(0.5)
108
+ if self.left_arm:
109
+ self.left_arm.DisConnectPort()
110
+ if self.right_arm:
111
+ self.right_arm.DisConnectPort()
112
+ self.connected = False
113
+
114
+ def _state_update_loop(self):
115
+ """Continuously update the robot state in a background thread."""
116
+ while self._running:
117
+ try:
118
+ left_pose = self.left_arm.GetArmEndPoseMsgs().end_pose
119
+ right_pose = self.right_arm.GetArmEndPoseMsgs().end_pose
120
+ left_joint = self.left_arm.GetArmJointMsgs().joint_state
121
+
122
+ right_joint = self.right_arm.GetArmJointMsgs().joint_state
123
+ left_ctrl = self.left_arm.GetArmJointCtrl().joint_ctrl
124
+ right_ctrl = self.right_arm.GetArmJointCtrl().joint_ctrl
125
+ # print(left_ctrl)
126
+ if left_pose:
127
+ self._state_cache["left_arm"]["qpos"] = [
128
+ left_pose.X_axis / 1000000,
129
+ left_pose.Y_axis / 1000000,
130
+ left_pose.Z_axis / 1000000,
131
+ left_pose.RX_axis / 57295.7795,
132
+ left_pose.RY_axis / 57295.7795,
133
+ left_pose.RZ_axis / 57295.7795,
134
+ ]
135
+ if left_ctrl:
136
+ self._state_cache["left_ctrl"]["qpos"] = [
137
+ left_ctrl.joint_1 / 57295.7795,
138
+ left_ctrl.joint_2 / 57295.7795,
139
+ left_ctrl.joint_3 / 57295.7795,
140
+ left_ctrl.joint_4 / 57295.7795,
141
+ left_ctrl.joint_5 / 57295.7795,
142
+ left_ctrl.joint_6 / 57295.7795,
143
+ ]
144
+ if left_joint:
145
+ self._state_cache["left_joint"]["qpos"] = [
146
+ left_joint.joint_1 / 57295.7795,
147
+ left_joint.joint_2 / 57295.7795,
148
+ left_joint.joint_3 / 57295.7795,
149
+ left_joint.joint_4 / 57295.7795,
150
+ left_joint.joint_5 / 57295.7795,
151
+ left_joint.joint_6 / 57295.7795,
152
+ ]
153
+ if right_pose:
154
+ self._state_cache["right_arm"]["qpos"] = [
155
+ right_pose.X_axis / 1000000,
156
+ right_pose.Y_axis / 1000000,
157
+ right_pose.Z_axis / 1000000,
158
+ right_pose.RX_axis / 57295.7795,
159
+ right_pose.RY_axis / 57295.7795,
160
+ right_pose.RZ_axis / 57295.7795,
161
+ ]
162
+ if right_ctrl:
163
+ self._state_cache["right_ctrl"]["qpos"] = [
164
+ right_ctrl.joint_1 / 57295.7795,
165
+ right_ctrl.joint_2 / 57295.7795,
166
+ right_ctrl.joint_3 / 57295.7795,
167
+ right_ctrl.joint_4 / 57295.7795,
168
+ right_ctrl.joint_5 / 57295.7795,
169
+ right_ctrl.joint_6 / 57295.7795,
170
+ ]
171
+ if right_joint:
172
+ self._state_cache["right_joint"]["qpos"] = [
173
+ right_joint.joint_1 / 57295.7795,
174
+ right_joint.joint_2 / 57295.7795,
175
+ right_joint.joint_3 / 57295.7795,
176
+ right_joint.joint_4 / 57295.7795,
177
+ right_joint.joint_5 / 57295.7795,
178
+ right_joint.joint_6 / 57295.7795,
179
+ ]
180
+ left_grip = self.left_arm.GetArmGripperMsgs().gripper_state
181
+ right_grip = self.right_arm.GetArmGripperMsgs().gripper_state
182
+
183
+ if left_grip:
184
+ self._state_cache["left_gripper"] = [left_grip.grippers_angle / 1e6]
185
+
186
+ if right_grip:
187
+ self._state_cache["right_gripper"] = [
188
+ right_grip.grippers_angle / 1e6
189
+ ]
190
+
191
+ except Exception as e:
192
+ logger.error(f"Error updating robot state: {e}")
193
+ pass
194
+
195
+ time.sleep(0.02)
196
+
197
+ def set_gripper(
198
+ self,
199
+ left_value: float,
200
+ right_value: float,
201
+ speed: int = 1000,
202
+ force: int = 0,
203
+ ):
204
+ """Control the grippers of the robot."""
205
+ max_open_um = 50000
206
+
207
+ left_pos = int((1.0 - left_value) * max_open_um)
208
+ right_pos = int((1.0 - right_value) * max_open_um)
209
+ self._state_cache["left_vr_pose"]["qpos"][6] = left_pos / 1000000.0
210
+ self._state_cache["right_vr_pose"]["qpos"][6] = right_pos / 1000000.0
211
+ self.left_arm.GripperCtrl(abs(left_pos), speed, 0x01, force)
212
+ self.right_arm.GripperCtrl(abs(right_pos), speed, 0x01, force)
213
+
214
+ def move_to_joint(self, action_joint):
215
+ """Move the robot to specific joint positions."""
216
+ factor = 57295.7795
217
+ if action_joint is not None:
218
+ joint_0 = round(action_joint[0] * factor)
219
+ joint_1 = round(action_joint[1] * factor)
220
+ joint_2 = round(action_joint[2] * factor)
221
+ joint_3 = round(action_joint[3] * factor)
222
+ joint_4 = round(action_joint[4] * factor)
223
+ joint_5 = round(action_joint[5] * factor)
224
+ joint_6 = round(action_joint[6] * 1000 * 1000)
225
+ self.left_arm.MotionCtrl_2(0x01, 0x01, 100, 0x00)
226
+ self.left_arm.JointCtrl(
227
+ joint_0, joint_1, joint_2, joint_3, joint_4, joint_5
228
+ )
229
+ self.left_arm.GripperCtrl(abs(joint_6), 1000, 0x01, 0)
230
+ joint_0 = round(action_joint[7] * factor)
231
+ joint_1 = round(action_joint[8] * factor)
232
+ joint_2 = round(action_joint[9] * factor)
233
+ joint_3 = round(action_joint[10] * factor)
234
+ joint_4 = round(action_joint[11] * factor)
235
+ joint_5 = round(action_joint[12] * factor)
236
+ joint_6 = round(action_joint[13] * 1000 * 1000)
237
+ self.right_arm.MotionCtrl_2(0x01, 0x01, 100, 0x00)
238
+ self.right_arm.JointCtrl(
239
+ joint_0, joint_1, joint_2, joint_3, joint_4, joint_5
240
+ )
241
+ self.right_arm.GripperCtrl(abs(joint_6), 1000, 0x01, 0)
242
+
243
+ def move_to_pose(
244
+ self,
245
+ left_target: Optional[np.ndarray] = None,
246
+ right_target: Optional[np.ndarray] = None,
247
+ duration: float = 1,
248
+ ):
249
+ """Move the robot to specific end-effector poses."""
250
+ factor = 1000.0
251
+
252
+ if left_target is not None:
253
+ pos = left_target[:3, 3] * 1000.0
254
+ rot = np.dot(
255
+ left_target[:3, :3], np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
256
+ )
257
+ rot = R.from_matrix(rot).as_euler("xyz", degrees=True)
258
+ # print(f"pos:{pos}, rot: {rot}")
259
+ self._state_cache["left_vr_pose"]["qpos"] = [
260
+ pos[0] / 1000.0,
261
+ pos[1] / 1000.0,
262
+ pos[2] / 1000.0,
263
+ rot[0],
264
+ rot[1],
265
+ rot[2],
266
+ self._state_cache["left_vr_pose"]["qpos"][6],
267
+ ]
268
+ # rot = [0, 85, 0]
269
+
270
+ self.left_arm.MotionCtrl_2(0x01, 0x00, int(duration * 100), 0x00)
271
+ self.left_arm.EndPoseCtrl(
272
+ int(pos[0] * factor),
273
+ int(pos[1] * factor),
274
+ int(pos[2] * factor),
275
+ int(rot[0] * factor),
276
+ int(rot[1] * factor),
277
+ int(rot[2] * factor),
278
+ )
279
+
280
+ if right_target is not None:
281
+ pos = right_target[:3, 3] * 1000.0
282
+ rot = np.dot(
283
+ right_target[:3, :3], np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
284
+ )
285
+ rot = R.from_matrix(rot).as_euler("xyz", degrees=True)
286
+ self._state_cache["right_vr_pose"]["qpos"] = [
287
+ pos[0] / 1000.0,
288
+ pos[1] / 1000.0,
289
+ pos[2] / 1000.0,
290
+ rot[0],
291
+ rot[1],
292
+ rot[2],
293
+ self._state_cache["right_vr_pose"]["qpos"][6],
294
+ ]
295
+ # print(f"pos_R:{pos}, rot_R: {rot}")
296
+ self.right_arm.MotionCtrl_2(0x01, 0x00, int(duration * 100), 0x00)
297
+ self.right_arm.EndPoseCtrl(
298
+ int(pos[0] * factor),
299
+ int(pos[1] * factor),
300
+ int(pos[2] * factor),
301
+ int(rot[0] * factor),
302
+ int(rot[1] * factor),
303
+ int(rot[2] * factor),
304
+ )
305
+
306
+ def go_home(self):
307
+ """Move the robot to its home position."""
308
+ self.left_arm.MotionCtrl_2(0x01, 0x01, 30, 0x00)
309
+ self.left_arm.JointCtrl(0, 0, 0, 0, 0, 0)
310
+ self.right_arm.MotionCtrl_2(0x01, 0x01, 30, 0x00)
311
+ self.right_arm.JointCtrl(0, 0, 0, 0, 0, 0)
312
+ self._state_cache["left_vr_pose"]["qpos"] = [
313
+ 0.057,
314
+ 0,
315
+ 0.215,
316
+ 0,
317
+ 1.4835,
318
+ 0,
319
+ 0.05,
320
+ ]
321
+ self._state_cache["right_vr_pose"]["qpos"] = [
322
+ 0.057,
323
+ 0,
324
+ 0.215,
325
+ 0,
326
+ 1.4835,
327
+ 0,
328
+ 0.05,
329
+ ]
330
+
331
+ def safe_stop(self):
332
+ """Safely stop the robot motion."""
333
+ self.left_arm.EmergencyStop()
334
+ self.right_arm.EmergencyStop()
335
+
336
+ def get_state_pos(self):
337
+ """Get the current joint positions."""
338
+ # print(f"self.jstate_cache{self._state_cache}")
339
+ return (
340
+ self._state_cache["left_joint"]["qpos"]
341
+ + self._state_cache["left_gripper"]
342
+ + self._state_cache["right_joint"]["qpos"]
343
+ + self._state_cache["right_gripper"]
344
+ )
345
+
346
+ def get_current_state(self):
347
+ """Get the complete current state of the robot."""
348
+ return {
349
+ "left_arm": self._state_cache["left_arm"],
350
+ "right_arm": self._state_cache["right_arm"],
351
+ "left_gripper": self._state_cache["left_gripper"],
352
+ "right_gripper": self._state_cache["right_gripper"],
353
+ "left_joint": self._state_cache["left_joint"],
354
+ "right_joint": self._state_cache["right_joint"],
355
+ "left_ctrl": self._state_cache["left_ctrl"],
356
+ "right_ctrl": self._state_cache["right_ctrl"],
357
+ "left_vr_pose": self._state_cache["left_vr_pose"],
358
+ "right_vr_pose": self._state_cache["right_vr_pose"],
359
+ "body": {"qpos": []},
360
+ }
File without changes
@@ -0,0 +1,43 @@
1
+ """
2
+ Sensors module for SuperBot
3
+ Contains classes and utilities for managing robot sensors.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ import time
8
+ from typing import Dict, Any, Optional
9
+ import numpy as np
10
+
11
+
12
+ class BaseSensor(ABC):
13
+ """Abstract base class for all sensors."""
14
+
15
+ def __init__(self, name: str, sensor_type: str):
16
+ self.name = name
17
+ self.sensor_type = sensor_type
18
+ self.last_reading_time = 0
19
+ self.is_connected = False
20
+
21
+ @abstractmethod
22
+ def connect(self):
23
+ """Connect to the sensor."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ def disconnect(self):
28
+ """Disconnect from the sensor."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def read(self) -> Dict[str, Any]:
33
+ """Read data from the sensor."""
34
+ pass
35
+
36
+ def get_status(self) -> Dict[str, Any]:
37
+ """Get sensor status information."""
38
+ return {
39
+ "name": self.name,
40
+ "type": self.sensor_type,
41
+ "connected": self.is_connected,
42
+ "last_reading_time": self.last_reading_time,
43
+ }
File without changes
@@ -0,0 +1,140 @@
1
+ import numpy as np
2
+ import cv2
3
+ from typing import Any, cast
4
+
5
+ from loguru import logger
6
+
7
+ try:
8
+ import pyrealsense2 as rs
9
+ except ImportError:
10
+ print("Warning: pyrealsense2 not available. Camera functionality will be limited.")
11
+ rs = None
12
+
13
+ # Help static analyzers: treat rs as dynamic Any when available
14
+ if rs is not None:
15
+ rs = cast(Any, rs)
16
+
17
+
18
+ class CameraWrapper:
19
+ def __init__(
20
+ self,
21
+ devices=None,
22
+ width=640,
23
+ height=480,
24
+ fps=30,
25
+ num_realsense=0,
26
+ cv_format="MJPEG",
27
+ ):
28
+ self.width = width
29
+ self.height = height
30
+ self.fps = fps
31
+ self.num_realsense = max(0, int(num_realsense))
32
+ self.cv_format = cv_format
33
+ self.cameras = [] # list of dicts: {type: 'rs'|'cv', handle: pipeline|cap}
34
+ self.device_ids = devices if devices is not None else []
35
+ self._open_cameras()
36
+ print(f"successfully opened {len(self.cameras)} cameras!")
37
+
38
+ def _open_cameras(self):
39
+ if not self.device_ids:
40
+ print("No devices provided for CameraWrapper")
41
+ return
42
+
43
+ for idx, dev in enumerate(self.device_ids):
44
+ # Decide camera type
45
+ use_realsense = idx < self.num_realsense
46
+
47
+ if use_realsense:
48
+ if rs is None:
49
+ print(
50
+ f"pyrealsense2 not available, skipping RealSense device at index {idx} (id: {dev})"
51
+ )
52
+ continue
53
+ try:
54
+ serial = str(dev)
55
+ pipeline = rs.pipeline() # type: ignore[attr-defined]
56
+ config = rs.config() # type: ignore[attr-defined]
57
+ config.enable_device(serial)
58
+ config.enable_stream(rs.stream.color, self.width, self.height, rs.format.bgr8, self.fps) # type: ignore[attr-defined]
59
+ pipeline.start(config)
60
+ self.cameras.append({"type": "rs", "handle": pipeline})
61
+ print(f"RealSense camera {serial} opened successfully")
62
+ except Exception as e:
63
+ print(f"Failed to open RealSense camera {dev}: {e}")
64
+ else:
65
+ try:
66
+ device_index = int(dev)
67
+ print(f"Ready to read deive: {device_index}")
68
+ cap = cv2.VideoCapture(device_index)
69
+
70
+ if self.cv_format == "MJPEG":
71
+ cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG")) # type: ignore[attr-defined]
72
+ elif self.cv_format == "YUYV":
73
+ cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV")) # type: ignore[attr-defined]
74
+
75
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
76
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
77
+ cap.set(cv2.CAP_PROP_FPS, self.fps)
78
+
79
+ if not cap.isOpened():
80
+ raise ValueError(f"Cannot open OpenCV camera {device_index}")
81
+
82
+ self.cameras.append({"type": "cv", "handle": cap})
83
+ print(f"OpenCV camera {device_index} opened successfully")
84
+ except Exception as e:
85
+ print(f"Failed to open OpenCV camera {dev}: {e}")
86
+
87
+ def get_images(self):
88
+ images = []
89
+ if len(self.cameras) == 0:
90
+ # Return dummy images if no cameras available - use 640x480 which is expected by the model
91
+ for _ in range(max(1, len(self.device_ids))):
92
+ dummy_img = np.zeros((self.height, self.width, 3), dtype=np.uint8)
93
+ dummy_img[:, :, :] = 128 # Gray color instead of black
94
+ images.append(dummy_img)
95
+ return images
96
+
97
+ for cam in self.cameras:
98
+ if cam["type"] == "rs":
99
+ try:
100
+ pipeline = cam["handle"]
101
+ frames = pipeline.wait_for_frames()
102
+ color_frame = frames.get_color_frame()
103
+ if not color_frame:
104
+ dummy_img = np.zeros(
105
+ (self.height, self.width, 3), dtype=np.uint8
106
+ )
107
+ dummy_img[:, :, :] = 128
108
+ images.append(dummy_img)
109
+ else:
110
+ img = np.asanyarray(color_frame.get_data())
111
+ images.append(img)
112
+ except Exception as e:
113
+ print(f"Error reading from RealSense: {e}")
114
+ dummy_img = np.zeros((self.height, self.width, 3), dtype=np.uint8)
115
+ dummy_img[:, :, :] = 128
116
+ images.append(dummy_img)
117
+ elif cam["type"] == "cv":
118
+ cap = cam["handle"]
119
+ ret, frame = cap.read()
120
+ if not ret or frame is None:
121
+ dummy_img = np.zeros((self.height, self.width, 3), dtype=np.uint8)
122
+ dummy_img[:, :, :] = 128
123
+ images.append(dummy_img)
124
+ else:
125
+ images.append(frame)
126
+ return images
127
+
128
+ def release(self):
129
+ for cam in self.cameras:
130
+ if cam["type"] == "rs":
131
+ try:
132
+ cam["handle"].stop()
133
+ except Exception:
134
+ pass
135
+ elif cam["type"] == "cv":
136
+ try:
137
+ cam["handle"].release()
138
+ except Exception:
139
+ pass
140
+ self.cameras = []
@@ -0,0 +1,203 @@
1
+ """
2
+ Utils module for SuperBot
3
+ Contains utility functions and classes for common robotic operations.
4
+ """
5
+
6
+ import numpy as np
7
+ from scipy.spatial.transform import Rotation as R
8
+ import json
9
+ import yaml
10
+ from typing import Union, Dict, Any, Tuple
11
+ import time
12
+ import logging
13
+ from pathlib import Path
14
+
15
+
16
+ def skew_symmetric(v):
17
+ """
18
+ Create skew-symmetric matrix from vector
19
+ For a vector v = [v1, v2, v3], returns:
20
+ [[0, -v3, v2],
21
+ [v3, 0, -v1],
22
+ [-v2, v1, 0]]
23
+ """
24
+ return np.array([
25
+ [0, -v[2], v[1]],
26
+ [v[2], 0, -v[0]],
27
+ [-v[1], v[0], 0]
28
+ ])
29
+
30
+
31
+ def rotation_matrix_to_euler(R_mat: np.ndarray, sequence: str = 'xyz') -> np.ndarray:
32
+ """Convert rotation matrix to Euler angles."""
33
+ r = R.from_matrix(R_mat)
34
+ return r.as_euler(sequence, degrees=True)
35
+
36
+
37
+ def euler_to_rotation_matrix(euler_angles: np.ndarray, sequence: str = 'xyz') -> np.ndarray:
38
+ """Convert Euler angles to rotation matrix."""
39
+ r = R.from_euler(sequence, euler_angles, degrees=True)
40
+ return r.as_matrix()
41
+
42
+
43
+ def quaternion_to_rotation_matrix(quat: np.ndarray) -> np.ndarray:
44
+ """Convert quaternion [x, y, z, w] to rotation matrix."""
45
+ r = R.from_quat(quat)
46
+ return r.as_matrix()
47
+
48
+
49
+ def rotation_matrix_to_quaternion(R_mat: np.ndarray) -> np.ndarray:
50
+ """Convert rotation matrix to quaternion [x, y, z, w]."""
51
+ r = R.from_matrix(R_mat)
52
+ return r.as_quat()
53
+
54
+
55
+ def transformation_matrix(rotation: np.ndarray, translation: np.ndarray) -> np.ndarray:
56
+ """Create a 4x4 transformation matrix from rotation matrix and translation vector."""
57
+ T = np.eye(4)
58
+ T[:3, :3] = rotation
59
+ T[:3, 3] = translation
60
+ return T
61
+
62
+
63
+ def get_inverse_transform(T: np.ndarray) -> np.ndarray:
64
+ """Compute inverse of a transformation matrix."""
65
+ R_inv = T[:3, :3].T
66
+ t_inv = -R_inv @ T[:3, 3]
67
+ T_inv = np.eye(4)
68
+ T_inv[:3, :3] = R_inv
69
+ T_inv[:3, 3] = t_inv
70
+ return T_inv
71
+
72
+
73
+ def interpolate_positions(start_pos: np.ndarray, end_pos: np.ndarray, t: float) -> np.ndarray:
74
+ """
75
+ Linearly interpolate between two positions.
76
+ t should be between 0 and 1, where 0 is start_pos and 1 is end_pos.
77
+ """
78
+ if not 0 <= t <= 1:
79
+ raise ValueError("Interpolation parameter t must be between 0 and 1")
80
+ return start_pos + t * (end_pos - start_pos)
81
+
82
+
83
+ def normalize_quaternion(quat: np.ndarray) -> np.ndarray:
84
+ """Normalize a quaternion."""
85
+ return quat / np.linalg.norm(quat)
86
+
87
+
88
+ def compute_jacobian(robot_model, joint_angles: np.ndarray, link_idx: int) -> np.ndarray:
89
+ """
90
+ Compute the Jacobian matrix for a given robot configuration.
91
+ This is a simplified version - a full implementation would depend on the specific kinematic model.
92
+ """
93
+ # This is a placeholder implementation
94
+ # Real implementation would require forward kinematics and geometric Jacobian calculation
95
+ num_joints = len(joint_angles)
96
+ jacobian = np.zeros((6, num_joints)) # [linear_velocities, angular_velocities]
97
+
98
+ # Placeholder computation
99
+ return jacobian
100
+
101
+
102
+ def load_config(config_path: Union[str, Path]) -> Dict[str, Any]:
103
+ """Load configuration from YAML or JSON file."""
104
+ config_path = Path(config_path)
105
+
106
+ if config_path.suffix.lower() in ['.yaml', '.yml']:
107
+ with open(config_path, 'r') as f:
108
+ return yaml.safe_load(f)
109
+ elif config_path.suffix.lower() == '.json':
110
+ with open(config_path, 'r') as f:
111
+ return json.load(f)
112
+ else:
113
+ raise ValueError(f"Unsupported config file format: {config_path.suffix}")
114
+
115
+
116
+ def save_config(config: Dict[str, Any], config_path: Union[str, Path]):
117
+ """Save configuration to YAML or JSON file."""
118
+ config_path = Path(config_path)
119
+
120
+ if config_path.suffix.lower() in ['.yaml', '.yml']:
121
+ with open(config_path, 'w') as f:
122
+ yaml.dump(config, f, default_flow_style=False)
123
+ elif config_path.suffix.lower() == '.json':
124
+ with open(config_path, 'w') as f:
125
+ json.dump(config, f, indent=2)
126
+ else:
127
+ raise ValueError(f"Unsupported config file format: {config_path.suffix}")
128
+
129
+
130
+ class Timer:
131
+ """Simple timer utility for measuring execution time."""
132
+
133
+ def __init__(self):
134
+ self.start_time = None
135
+ self.elapsed_time = 0
136
+
137
+ def start(self):
138
+ """Start the timer."""
139
+ self.start_time = time.time()
140
+
141
+ def stop(self) -> float:
142
+ """Stop the timer and return elapsed time in seconds."""
143
+ if self.start_time is None:
144
+ raise RuntimeError("Timer not started")
145
+ self.elapsed_time = time.time() - self.start_time
146
+ self.start_time = None
147
+ return self.elapsed_time
148
+
149
+ def reset(self):
150
+ """Reset the timer."""
151
+ self.start_time = None
152
+ self.elapsed_time = 0
153
+
154
+
155
+ def setup_logging(level: str = "INFO", log_file: str = None):
156
+ """Setup logging configuration."""
157
+ level_enum = getattr(logging, level.upper())
158
+
159
+ handlers = []
160
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
161
+
162
+ console_handler = logging.StreamHandler()
163
+ console_handler.setLevel(level_enum)
164
+ console_handler.setFormatter(formatter)
165
+ handlers.append(console_handler)
166
+
167
+ if log_file:
168
+ file_handler = logging.FileHandler(log_file)
169
+ file_handler.setLevel(level_enum)
170
+ file_handler.setFormatter(formatter)
171
+ handlers.append(file_handler)
172
+
173
+ logging.basicConfig(level=level_enum, handlers=handlers)
174
+
175
+
176
+ def validate_pose(pose: np.ndarray) -> bool:
177
+ """Validate that the input is a valid 4x4 pose matrix."""
178
+ if pose.shape != (4, 4):
179
+ return False
180
+
181
+ # Check if upper-left 3x3 is a valid rotation matrix
182
+ R = pose[:3, :3]
183
+ if not np.allclose(R @ R.T, np.eye(3)):
184
+ return False
185
+
186
+ if not np.isclose(np.linalg.det(R), 1.0):
187
+ return False
188
+
189
+ return True
190
+
191
+
192
+ def smooth_trajectory(waypoints: np.ndarray, smoothing_factor: float = 0.1) -> np.ndarray:
193
+ """Apply smoothing to a trajectory defined by waypoints."""
194
+ if len(waypoints) < 3:
195
+ return waypoints
196
+
197
+ smoothed = np.copy(waypoints)
198
+
199
+ for i in range(1, len(waypoints) - 1):
200
+ smoothed[i] = (1 - smoothing_factor) * waypoints[i] + \
201
+ smoothing_factor * 0.5 * (waypoints[i-1] + waypoints[i+1])
202
+
203
+ return smoothed
@@ -0,0 +1,149 @@
1
+ import math
2
+ import threading
3
+ import queue
4
+ import time
5
+ import numpy as np
6
+
7
+ """
8
+ def worker(obs, horizon):
9
+
10
+ obs: 任意观测
11
+ return: np.array with shape (horizon, 14)
12
+
13
+
14
+
15
+ example usage:
16
+ rtc = RealtimeChunking(
17
+ worker=vla_worker,
18
+ horizon=32,
19
+ overlap=8,
20
+ )
21
+
22
+ while True:
23
+ obs = get_obs()
24
+ action = rtc.step(obs)
25
+ send_action(action)
26
+
27
+ """
28
+
29
+
30
+ class ActionChunk:
31
+ def __init__(self, actions):
32
+ self.actions = np.array(actions)
33
+
34
+ def __len__(self):
35
+ return len(self.actions)
36
+
37
+
38
+ class RealtimeChunkingSoft:
39
+ """
40
+ A simple realtime chunking implementation which can wraps any VLA model.
41
+ for REAL RTC as pi does, we need model supports action prefix.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ worker,
47
+ horizon,
48
+ overlap,
49
+ blend_mode="cosine", # "linear" | "cosine" | "minjerk"
50
+ blend_grippers=False,
51
+ gripper_indices=[6, 13],
52
+ ):
53
+ assert overlap > 0
54
+ assert overlap < horizon
55
+
56
+ self.worker = worker
57
+ self.horizon = horizon
58
+ self.overlap = overlap
59
+ self.blend_mode = blend_mode
60
+ self.blend_grippers = blend_grippers
61
+ self.gripper_indices = gripper_indices
62
+
63
+ self.current_chunk = None
64
+ self.step_idx = 0
65
+
66
+ self.infer_thread = None
67
+ self.infer_queue = queue.Queue(maxsize=1)
68
+
69
+ def _infer_async(self, obs):
70
+ def _run():
71
+ actions = self.worker(obs, self.horizon)
72
+ self.infer_queue.put(actions)
73
+
74
+ self.infer_thread = threading.Thread(target=_run, daemon=True)
75
+ self.infer_thread.start()
76
+
77
+ def _should_start_inference(self):
78
+ return (
79
+ self.infer_thread is None and self.step_idx >= self.horizon - self.overlap
80
+ )
81
+
82
+ def _alpha(self, i):
83
+ s = (i + 1) / self.overlap
84
+
85
+ if self.blend_mode == "linear":
86
+ return s
87
+
88
+ if self.blend_mode == "cosine":
89
+ return 0.5 * (1.0 - math.cos(math.pi * s))
90
+
91
+ if self.blend_mode == "minjerk":
92
+ return 10 * s**3 - 15 * s**4 + 6 * s**5
93
+
94
+ raise ValueError(f"Unknown blend_mode: {self.blend_mode}")
95
+
96
+ def _blend(self, old_tail, new_head):
97
+ blended = np.zeros_like(old_tail)
98
+
99
+ for i in range(self.overlap):
100
+ a = self._alpha(i)
101
+ blended[i] = (1.0 - a) * old_tail[i] + a * new_head[i]
102
+
103
+ if not self.blend_grippers:
104
+ blended[:, self.gripper_indices] = new_head[:, self.gripper_indices]
105
+
106
+ return blended
107
+
108
+ def _try_commit_next_chunk(self):
109
+ if self.step_idx < self.horizon:
110
+ return
111
+ if self.infer_queue.empty():
112
+ return
113
+
114
+ new_actions = self.infer_queue.get()
115
+
116
+ old_tail = self.current_chunk.actions[
117
+ self.horizon - self.overlap : self.horizon
118
+ ]
119
+ new_head = new_actions[: self.overlap]
120
+
121
+ blended = self._blend(old_tail, new_head)
122
+ merged = np.concatenate((blended, new_actions[self.overlap :]), axis=0)
123
+
124
+ self.current_chunk = ActionChunk(merged)
125
+ self.step_idx = 0
126
+ self.infer_thread = None
127
+
128
+ def reset(self):
129
+ self.current_chunk = None
130
+ self.step_idx = 0
131
+ self.infer_thread = None
132
+ while not self.infer_queue.empty():
133
+ self.infer_queue.get()
134
+
135
+ def step(self, obs):
136
+ if self.current_chunk is None:
137
+ actions = self.worker(obs, self.horizon)
138
+ self.current_chunk = ActionChunk(actions)
139
+ self.step_idx = 0
140
+
141
+ action = self.current_chunk.actions[self.step_idx]
142
+ self.step_idx += 1
143
+
144
+ if self._should_start_inference():
145
+ self._infer_async(obs)
146
+
147
+ self._try_commit_next_chunk()
148
+
149
+ return action
File without changes
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: superbot2
3
+ Version: 0.1.0
4
+ Summary: A modular robotics framework for controlling various robots
5
+ Author-email: SuperBot Team <team@superbot.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/superbot/superbot
8
+ Project-URL: Repository, https://github.com/superbot/superbot.git
9
+ Project-URL: Documentation, https://superbot.readthedocs.io/
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: numpy>=1.21.0
22
+ Requires-Dist: scipy>=1.7.0
23
+ Requires-Dist: PyYAML>=6.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=6.0; extra == "dev"
26
+ Requires-Dist: black>=22.0; extra == "dev"
27
+ Requires-Dist: flake8>=4.0; extra == "dev"
28
+ Requires-Dist: mypy>=0.950; extra == "dev"
29
+ Provides-Extra: docs
30
+ Requires-Dist: sphinx>=4.0; extra == "docs"
31
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
32
+
33
+ # SuperBot
34
+
35
+ SuperBot is a modern, modular robotics framework designed for controlling dual-arm piper robots and other robotic systems. It provides a clean, intuitive API for robot control, sensor integration, and utility functions.
36
+
37
+ ## Features
38
+
39
+ - Modular architecture with separate modules for bots, sensors, and utilities
40
+ - Dual-arm piper robot control interface
41
+ - Sensor management system
42
+ - Utility functions for robotics operations
43
+ - Clean, well-documented API
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install superbot
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from superbot import RobotWrapper
55
+
56
+ # Initialize the robot wrapper with CAN interfaces
57
+ robot = RobotWrapper(can_interfaces=("can_left", "can_right"))
58
+
59
+ # Move to a specific pose
60
+ import numpy as np
61
+ target_pose = np.eye(4) # 4x4 identity matrix as example
62
+ robot.move_to_pose(left_target=target_pose, right_target=target_pose)
63
+
64
+ # Close the grippers halfway
65
+ robot.set_gripper(0.5, 0.5)
66
+
67
+ # Get current robot state
68
+ state = robot.get_current_state()
69
+ print(state)
70
+ ```
71
+
72
+ ## Architecture
73
+
74
+ SuperBot follows a modular design:
75
+
76
+ - **bots**: Contains robot-specific implementations (dual_piper, etc.)
77
+ - **sensors**: Sensor management and interfaces
78
+ - **utils**: Common utility functions for robotics operations
79
+
80
+ ## Contributing
81
+
82
+ Contributions are welcome! Please see the contributing guidelines for details.
@@ -0,0 +1,15 @@
1
+ superbot/__init__.py,sha256=b9R5pWwWJujbsFqglG4sIxKvYFFBz-MzJmeIJDpOB98,71
2
+ superbot/bots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ superbot/bots/base_bot.py,sha256=e5FuYSpiikwrDYV9R3bQQNav1KTAYLYXwxww3Sz3D6U,2726
4
+ superbot/bots/dual_piper.py,sha256=weeFy5yM1My4TkZ9jeWN5s0sIrYm9tcN8lnUqLi10v8,13810
5
+ superbot/sensors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ superbot/sensors/base_sensor.py,sha256=9w9K7UyNBucFaLm8BOS7ApgiCtCcA7e49hDmuvHP_HY,1057
7
+ superbot/sensors/cams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ superbot/sensors/cams/cam_rs.py,sha256=peulTrdGQeulDDArD7OrG8Jd1AIj3lWrxK1lszYc2QE,5544
9
+ superbot/stabilization/__init__.py,sha256=odhzh4dok1cAGDf4rIJ9fVVE5djOwp6rMearJ-YMRg8,6346
10
+ superbot/stabilization/realtime_chunking.py,sha256=uAOlx7uj_BNPwl_fxe-prN1CPC7ZHzaIjjULfZUVRrs,3732
11
+ superbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ superbot2-0.1.0.dist-info/METADATA,sha256=OzzqwTrLuDCMCuFTzdK2tOcuDXTcLaO1_PC9pVNKCNM,2631
13
+ superbot2-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ superbot2-0.1.0.dist-info/top_level.txt,sha256=ydMxsX5c2ooKqU-gqzhLcqehv-2ZIrDRUrlqh-kpW14,9
15
+ superbot2-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ superbot