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 +5 -0
- superbot/bots/__init__.py +0 -0
- superbot/bots/base_bot.py +103 -0
- superbot/bots/dual_piper.py +360 -0
- superbot/sensors/__init__.py +0 -0
- superbot/sensors/base_sensor.py +43 -0
- superbot/sensors/cams/__init__.py +0 -0
- superbot/sensors/cams/cam_rs.py +140 -0
- superbot/stabilization/__init__.py +203 -0
- superbot/stabilization/realtime_chunking.py +149 -0
- superbot/utils/__init__.py +0 -0
- superbot2-0.1.0.dist-info/METADATA +82 -0
- superbot2-0.1.0.dist-info/RECORD +15 -0
- superbot2-0.1.0.dist-info/WHEEL +5 -0
- superbot2-0.1.0.dist-info/top_level.txt +1 -0
superbot/__init__.py
ADDED
|
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 @@
|
|
|
1
|
+
superbot
|