orca-core 0.3.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.
- orca_core/__init__.py +28 -0
- orca_core/api/__init__.py +0 -0
- orca_core/api/api.py +365 -0
- orca_core/base_hand.py +244 -0
- orca_core/calibration.py +78 -0
- orca_core/constants.py +76 -0
- orca_core/core.py +11 -0
- orca_core/demo_presets.py +99 -0
- orca_core/hand_config.py +402 -0
- orca_core/hardware/__init__.py +0 -0
- orca_core/hardware/dynamixel_client.py +911 -0
- orca_core/hardware/feetech/__init__.py +10 -0
- orca_core/hardware/feetech/group_sync_read.py +151 -0
- orca_core/hardware/feetech/group_sync_write.py +73 -0
- orca_core/hardware/feetech/hls.py +114 -0
- orca_core/hardware/feetech/port_handler.py +115 -0
- orca_core/hardware/feetech/protocol_packet_handler.py +565 -0
- orca_core/hardware/feetech/scscl.py +104 -0
- orca_core/hardware/feetech/scservo_def.py +27 -0
- orca_core/hardware/feetech/sms_sts.py +114 -0
- orca_core/hardware/feetech_client.py +778 -0
- orca_core/hardware/mock_dynamixel_client.py +522 -0
- orca_core/hardware/mock_tactile_client.py +283 -0
- orca_core/hardware/motor_client.py +172 -0
- orca_core/hardware/sensing/__init__.py +0 -0
- orca_core/hardware/sensing/constants.py +108 -0
- orca_core/hardware/sensing/protocol.py +544 -0
- orca_core/hardware/sensing/types.py +74 -0
- orca_core/hardware/tactile_client.py +775 -0
- orca_core/hardware_hand.py +1513 -0
- orca_core/joint_position.py +147 -0
- orca_core/utils/__init__.py +10 -0
- orca_core/utils/utils.py +338 -0
- orca_core/version.py +1 -0
- orca_core-0.3.0.dist-info/METADATA +114 -0
- orca_core-0.3.0.dist-info/RECORD +38 -0
- orca_core-0.3.0.dist-info/WHEEL +4 -0
- orca_core-0.3.0.dist-info/licenses/LICENSE +21 -0
orca_core/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Copyright (c) 2025 ORCA
|
|
3
|
+
#
|
|
4
|
+
# This file is part of ORCA and is licensed under the MIT License.
|
|
5
|
+
# You may use, copy, modify, and distribute this file under the terms of the MIT License.
|
|
6
|
+
# See the LICENSE file at the root of this repository for full license information.
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
from .calibration import CalibrationResult
|
|
9
|
+
from .hand_config import BaseHandConfig
|
|
10
|
+
from .hand_config import OrcaHandConfig
|
|
11
|
+
from .hand_config import OrcaHandTouchConfig
|
|
12
|
+
from .hand_config import canonical_joint_ids
|
|
13
|
+
from .hardware_hand import OrcaHand
|
|
14
|
+
from .hardware_hand import OrcaHandTouch
|
|
15
|
+
from .joint_position import OrcaJointPositions
|
|
16
|
+
from .version import LATEST_VERSION
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CalibrationResult",
|
|
20
|
+
"BaseHandConfig",
|
|
21
|
+
"OrcaHandConfig",
|
|
22
|
+
"OrcaHandTouchConfig",
|
|
23
|
+
"OrcaHand",
|
|
24
|
+
"OrcaHandTouch",
|
|
25
|
+
"OrcaJointPositions",
|
|
26
|
+
"canonical_joint_ids",
|
|
27
|
+
"LATEST_VERSION",
|
|
28
|
+
]
|
|
File without changes
|
orca_core/api/api.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Copyright (c) 2025 ORCA
|
|
3
|
+
#
|
|
4
|
+
# This file is part of ORCA and is licensed under the MIT License.
|
|
5
|
+
# You may use, copy, modify, and distribute this file under the terms of the MIT License.
|
|
6
|
+
# See the LICENSE file at the root of this repository for full license information.
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from fastapi import FastAPI, HTTPException, Body
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
from typing import List, Dict, Optional, Union, Tuple
|
|
15
|
+
import numpy as np
|
|
16
|
+
import uvicorn
|
|
17
|
+
from orca_core.utils.yaml_utils import read_yaml, update_yaml
|
|
18
|
+
|
|
19
|
+
from orca_core import OrcaHand
|
|
20
|
+
|
|
21
|
+
app = FastAPI(title="OrcaHand API", version="1.0.0")
|
|
22
|
+
|
|
23
|
+
# --- Global OrcaHand Instance ---
|
|
24
|
+
# Ensure necessary config files are present or adjust path as needed
|
|
25
|
+
|
|
26
|
+
hand = OrcaHand()
|
|
27
|
+
|
|
28
|
+
class MotorList(BaseModel):
|
|
29
|
+
motor_ids: Optional[List[int]] = None
|
|
30
|
+
|
|
31
|
+
class MaxCurrent(BaseModel):
|
|
32
|
+
current: Union[float, List[float]]
|
|
33
|
+
|
|
34
|
+
class JointPositions(BaseModel):
|
|
35
|
+
positions: Dict[str, float] = Field(..., example={"index_flex": 0.5, "thumb_flex": 0.2})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def handle_hand_exception(e: Exception):
|
|
39
|
+
"""Translates OrcaHand runtime errors to HTTP exceptions."""
|
|
40
|
+
if isinstance(e, RuntimeError):
|
|
41
|
+
if "not connected" in str(e).lower():
|
|
42
|
+
raise HTTPException(status_code=409, detail=f"Hand operation failed: {e}")
|
|
43
|
+
elif "not calibrated" in str(e).lower():
|
|
44
|
+
raise HTTPException(status_code=409, detail=f"Hand operation failed: {e}")
|
|
45
|
+
else:
|
|
46
|
+
raise HTTPException(status_code=400, detail=f"Hand operation failed: {e}")
|
|
47
|
+
elif isinstance(e, ValueError):
|
|
48
|
+
raise HTTPException(status_code=422, detail=f"Invalid input: {e}")
|
|
49
|
+
else:
|
|
50
|
+
raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --- API Endpoints ---
|
|
54
|
+
@app.post("/config", summary="Set Hand Configuration", tags=["Configuration"])
|
|
55
|
+
def set_hand_config(config_path: str = Body(..., example="/path/to/config")):
|
|
56
|
+
"""
|
|
57
|
+
Sets or updates the hand configuration by recreating the OrcaHand object.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config_path (str): Path to the new configuration file.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
dict: Success message.
|
|
64
|
+
"""
|
|
65
|
+
global hand, current_config_path
|
|
66
|
+
try:
|
|
67
|
+
if hand.is_connected():
|
|
68
|
+
hand.disconnect()
|
|
69
|
+
|
|
70
|
+
current_config_path = config_path
|
|
71
|
+
hand = OrcaHand(model_path=current_config_path)
|
|
72
|
+
return {"message": f"Hand configuration updated to: {config_path}"}
|
|
73
|
+
except Exception as e:
|
|
74
|
+
handle_hand_exception(e)
|
|
75
|
+
|
|
76
|
+
@app.post("/connect", summary="Connect to the OrcaHand", tags=["Connection"])
|
|
77
|
+
def connect_hand():
|
|
78
|
+
"""
|
|
79
|
+
Establishes a connection to the OrcaHand hardware.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict: Status message indicating success or failure.
|
|
83
|
+
"""
|
|
84
|
+
if hand.is_connected():
|
|
85
|
+
return {"message": "Hand already connected."}
|
|
86
|
+
try:
|
|
87
|
+
success, msg = hand.connect()
|
|
88
|
+
if success:
|
|
89
|
+
return {"message": msg}
|
|
90
|
+
else:
|
|
91
|
+
raise HTTPException(status_code=500, detail=f"Connection failed: {msg}")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
handle_hand_exception(e)
|
|
94
|
+
|
|
95
|
+
@app.post("/disconnect", summary="Disconnect from the OrcaHand", tags=["Connection"])
|
|
96
|
+
def disconnect_hand():
|
|
97
|
+
"""
|
|
98
|
+
Disconnects from the OrcaHand hardware, disabling torque first.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
dict: Status message indicating success or failure.
|
|
102
|
+
"""
|
|
103
|
+
if not hand.is_connected():
|
|
104
|
+
return {"message": "Hand already disconnected."}
|
|
105
|
+
try:
|
|
106
|
+
try:
|
|
107
|
+
hand.disable_torque()
|
|
108
|
+
time.sleep(0.1)
|
|
109
|
+
except Exception as torque_err:
|
|
110
|
+
print(f"Warning: Error disabling torque before disconnect: {torque_err}")
|
|
111
|
+
|
|
112
|
+
success, msg = hand.disconnect()
|
|
113
|
+
if success:
|
|
114
|
+
return {"message": msg}
|
|
115
|
+
else:
|
|
116
|
+
raise HTTPException(status_code=500, detail=f"Disconnection failed: {msg}")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
handle_hand_exception(e)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.get("/status", summary="Get Hand Status", tags=["Status"])
|
|
122
|
+
def get_status():
|
|
123
|
+
"""
|
|
124
|
+
Retrieves the current connection and calibration status of the hand.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
dict: Contains 'connected' (bool) and 'calibrated' (bool) status.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
return {
|
|
131
|
+
"connected": hand.is_connected(),
|
|
132
|
+
"calibrated": hand.is_calibrated() if hand.is_connected() else False
|
|
133
|
+
}
|
|
134
|
+
except Exception as e:
|
|
135
|
+
handle_hand_exception(e)
|
|
136
|
+
|
|
137
|
+
@app.post("/torque/enable", summary="Enable Motor Torque", tags=["Control"])
|
|
138
|
+
def enable_torque(motor_list: MotorList = Body(None)):
|
|
139
|
+
"""
|
|
140
|
+
Enables torque for specified motors or all motors if none are specified.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
motor_list (MotorList, optional): A JSON body containing a list of motor IDs.
|
|
144
|
+
Example: {"motor_ids": [1, 3]}
|
|
145
|
+
If omitted or null, torque is enabled for all motors.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict: Success message.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
ids = motor_list.motor_ids if motor_list else None
|
|
152
|
+
hand.enable_torque(motor_ids=ids)
|
|
153
|
+
return {"message": f"Torque enabled for motors: {ids or 'all'}"}
|
|
154
|
+
except Exception as e:
|
|
155
|
+
handle_hand_exception(e)
|
|
156
|
+
|
|
157
|
+
@app.post("/torque/disable", summary="Disable Motor Torque", tags=["Control"])
|
|
158
|
+
def disable_torque(motor_list: MotorList = Body(None)):
|
|
159
|
+
"""
|
|
160
|
+
Disables torque for specified motors or all motors if none are specified.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
motor_list (MotorList, optional): A JSON body containing a list of motor IDs.
|
|
164
|
+
Example: {"motor_ids": [1, 3]}
|
|
165
|
+
If omitted or null, torque is disabled for all motors.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
dict: Success message.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
ids = motor_list.motor_ids if motor_list else None
|
|
172
|
+
hand.disable_torque(motor_ids=ids)
|
|
173
|
+
return {"message": f"Torque disabled for motors: {ids or 'all'}"}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
handle_hand_exception(e)
|
|
176
|
+
|
|
177
|
+
@app.post("/current/max", summary="Set Maximum Motor Current", tags=["Control"])
|
|
178
|
+
def set_max_current(max_current: MaxCurrent):
|
|
179
|
+
"""
|
|
180
|
+
Sets the maximum current limit for the motors.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
max_current (MaxCurrent): A JSON body containing either a single float
|
|
184
|
+
(applied to all motors) or a list of floats
|
|
185
|
+
(one per motor, in order).
|
|
186
|
+
Example single: {"current": 300.0}
|
|
187
|
+
Example list: {"current": [300.0, 350.0, 300.0]}
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
dict: Success message.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
hand.set_max_current(current=max_current.current)
|
|
194
|
+
return {"message": "Maximum current set successfully."}
|
|
195
|
+
except Exception as e:
|
|
196
|
+
handle_hand_exception(e)
|
|
197
|
+
|
|
198
|
+
@app.get("/motors/position", summary="Get Motor Positions", tags=["State"])
|
|
199
|
+
def get_motor_position():
|
|
200
|
+
"""
|
|
201
|
+
Retrieves the current position of all motors in radians.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict: Contains a list of motor positions: {"positions": [pos1, pos2, ...]}.
|
|
205
|
+
Returns null if not connected.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
pos = hand.get_motor_pos()
|
|
209
|
+
return {"positions": pos.tolist() if pos is not None else None}
|
|
210
|
+
except Exception as e:
|
|
211
|
+
handle_hand_exception(e)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.get("/motors/current", summary="Get Motor Currents", tags=["State"])
|
|
215
|
+
def get_motor_current():
|
|
216
|
+
"""
|
|
217
|
+
Retrieves the current current draw of all motors.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
dict: Contains a list of motor currents: {"currents": [cur1, cur2, ...]}.
|
|
221
|
+
Returns null if not connected.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
cur = hand.get_motor_current()
|
|
225
|
+
return {"currents": cur.tolist() if cur is not None else None}
|
|
226
|
+
except Exception as e:
|
|
227
|
+
handle_hand_exception(e)
|
|
228
|
+
|
|
229
|
+
@app.get("/motors/temperature", summary="Get Motor Temperatures", tags=["State"])
|
|
230
|
+
def get_motor_temperature():
|
|
231
|
+
"""
|
|
232
|
+
Retrieves the current temperature of all motors.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
dict: Contains a list of motor temperatures: {"temperatures": [temp1, temp2, ...]}.
|
|
236
|
+
Returns null if not connected.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
temp = hand.get_motor_temp()
|
|
240
|
+
return {"temperatures": temp.tolist() if temp is not None else None}
|
|
241
|
+
except Exception as e:
|
|
242
|
+
handle_hand_exception(e)
|
|
243
|
+
|
|
244
|
+
@app.get("/joints/position", summary="Get Joint Positions", tags=["State"])
|
|
245
|
+
def get_joint_position():
|
|
246
|
+
"""
|
|
247
|
+
Retrieves the current position of all calibrated joints.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
dict: A dictionary mapping joint names to their positions:
|
|
251
|
+
{"positions": {"joint1": pos1, "joint2": pos2, ...}}.
|
|
252
|
+
Returns null for positions if not connected or not calibrated.
|
|
253
|
+
Individual joint values might be null if that specific joint isn't calibrated yet.
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
j_pos = hand.get_joint_pos()
|
|
257
|
+
return {"positions": j_pos}
|
|
258
|
+
except Exception as e:
|
|
259
|
+
handle_hand_exception(e)
|
|
260
|
+
|
|
261
|
+
@app.post("/joints/position", summary="Set Joint Positions", tags=["Control"])
|
|
262
|
+
def set_joint_position(joint_positions: JointPositions):
|
|
263
|
+
"""
|
|
264
|
+
Sets the desired positions for specified joints. Requires calibration.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
joint_positions (JointPositions): A JSON body containing a dictionary
|
|
268
|
+
mapping joint names to desired positions (in radians).
|
|
269
|
+
Example: {"positions": {"index_flex": 1.0, "thumb_oppose": 0.5}}
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
dict: Success message.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
hand.set_joint_pos(joint_pos=joint_positions.positions)
|
|
276
|
+
return {"message": "Joint positions command sent successfully."}
|
|
277
|
+
except Exception as e:
|
|
278
|
+
handle_hand_exception(e)
|
|
279
|
+
|
|
280
|
+
@app.get("/calibrate/status", summary="Get Calibration Status", tags=["Calibration"])
|
|
281
|
+
def get_calibration_status():
|
|
282
|
+
"""
|
|
283
|
+
Checks if the hand is fully calibrated.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
dict: Contains 'calibrated' (bool) status.
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
return {"calibrated": hand.is_calibrated()}
|
|
290
|
+
except Exception as e:
|
|
291
|
+
handle_hand_exception(e)
|
|
292
|
+
|
|
293
|
+
@app.post("/calibrate")
|
|
294
|
+
def calibrate_auto():
|
|
295
|
+
"""
|
|
296
|
+
Starts the automatic calibration routine defined in the configuration.
|
|
297
|
+
This might take some time.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
dict: Message indicating calibration start and eventual result.
|
|
301
|
+
"""
|
|
302
|
+
if not hand.is_connected():
|
|
303
|
+
raise HTTPException(status_code=409, detail="Hand must be connected to calibrate.")
|
|
304
|
+
try:
|
|
305
|
+
hand.calibrate()
|
|
306
|
+
calib_status = hand.is_calibrated()
|
|
307
|
+
msg = "Automatic calibration finished." + (" Successfully." if calib_status else " Failed or incomplete.")
|
|
308
|
+
return {"message": msg, "calibrated": calib_status}
|
|
309
|
+
except Exception as e:
|
|
310
|
+
handle_hand_exception(e)
|
|
311
|
+
|
|
312
|
+
# @app.get("/config/settings", summary="Get Current Configuration Settings", tags=["Configuration"])
|
|
313
|
+
# def get_config_settings():
|
|
314
|
+
# """
|
|
315
|
+
# Retrieves the current configuration settings from the file.
|
|
316
|
+
|
|
317
|
+
# Returns:
|
|
318
|
+
# dict: Current configuration settings.
|
|
319
|
+
# """
|
|
320
|
+
# global current_config_path
|
|
321
|
+
# try:
|
|
322
|
+
# if not current_config_path:
|
|
323
|
+
# raise HTTPException(status_code=400, detail="No configuration file is currently loaded.")
|
|
324
|
+
# config_data = read_yaml(current_config_path)
|
|
325
|
+
# return {"config": config_data}
|
|
326
|
+
# except Exception as e:
|
|
327
|
+
# handle_hand_exception(e)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# @app.put("/config/settings", summary="Update Configuration Settings", tags=["Configuration"])
|
|
331
|
+
# def update_config_settings(updated_settings: dict = Body(...)):
|
|
332
|
+
# """
|
|
333
|
+
# Updates specific settings in the configuration file and reloads the OrcaHand object.
|
|
334
|
+
|
|
335
|
+
# Args:
|
|
336
|
+
# updated_settings (dict): A dictionary containing the settings to update.
|
|
337
|
+
|
|
338
|
+
# Returns:
|
|
339
|
+
# dict: Success message.
|
|
340
|
+
# """
|
|
341
|
+
# global hand, current_config_path
|
|
342
|
+
# try:
|
|
343
|
+
# if not current_config_path:
|
|
344
|
+
# raise HTTPException(status_code=400, detail="No configuration file is currently loaded.")
|
|
345
|
+
|
|
346
|
+
# # Read the current configuration
|
|
347
|
+
# config_data = read_yaml(current_config_path)
|
|
348
|
+
|
|
349
|
+
# # Update the configuration with the new settings
|
|
350
|
+
# config_data.update(updated_settings)
|
|
351
|
+
|
|
352
|
+
# # Write the updated configuration back to the file
|
|
353
|
+
# write_config_file(current_config_path, config_data)
|
|
354
|
+
|
|
355
|
+
# # Reinitialize the OrcaHand object with the updated configuration
|
|
356
|
+
# if hand.is_connected():
|
|
357
|
+
# hand.disconnect()
|
|
358
|
+
# hand = OrcaHand(model_path=current_config_path)
|
|
359
|
+
|
|
360
|
+
# return {"message": "Configuration updated successfully.", "updated_config": config_data}
|
|
361
|
+
# except Exception as e:
|
|
362
|
+
# handle_hand_exception(e)
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
orca_core/base_hand.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Copyright (c) 2025 ORCA
|
|
3
|
+
#
|
|
4
|
+
# This file is part of ORCA and is licensed under the MIT License.
|
|
5
|
+
# You may use, copy, modify, and distribute this file under the terms of the MIT License.
|
|
6
|
+
# See the LICENSE file at the root of this repository for full license information.
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .demo_presets import DEMO_POSE_FRACTIONS, DEMO_SEQUENCES
|
|
16
|
+
from .hand_config import BaseHandConfig
|
|
17
|
+
from .joint_position import OrcaJointPositions
|
|
18
|
+
|
|
19
|
+
from .constants import NUM_STEPS, STEP_SIZE
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseHand(ABC):
|
|
23
|
+
"""Abstract base class defining the shared joint-space interface for all ORCA hand backends.
|
|
24
|
+
|
|
25
|
+
Concrete subclasses implement :meth:`_get_joint_positions` and
|
|
26
|
+
:meth:`_set_joint_positions`.
|
|
27
|
+
All higher-level motion helpers (interpolation, normalisation, neutral
|
|
28
|
+
position) live here so they are available regardless of backend.
|
|
29
|
+
|
|
30
|
+
On construction the hand loads its :class:`~orca_core.BaseHandConfig`,
|
|
31
|
+
validates it, and registers its default joint ordering with
|
|
32
|
+
:class:`~orca_core.OrcaJointPosition`.
|
|
33
|
+
|
|
34
|
+
The class exposes methods for:
|
|
35
|
+
- Setting joint positions, thus reaching arbitrary hand specifications
|
|
36
|
+
- Recording and replaying named positions
|
|
37
|
+
- Moving to the neutral position defined in the hand configuration file, ``config.yaml``.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config_path: Path to a ``config.yaml`` file. When ``None`` the default
|
|
41
|
+
model bundled with the package is used.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
config_cls = BaseHandConfig
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
config_path: str | None = None,
|
|
49
|
+
config: BaseHandConfig | None = None,
|
|
50
|
+
model_version: str | None = None,
|
|
51
|
+
model_name: str | None = None,
|
|
52
|
+
**config_kwargs,
|
|
53
|
+
):
|
|
54
|
+
self.config = (
|
|
55
|
+
config
|
|
56
|
+
if config is not None
|
|
57
|
+
else self.config_cls.from_config_path(
|
|
58
|
+
config_path=config_path,
|
|
59
|
+
model_version=model_version,
|
|
60
|
+
model_name=model_name,
|
|
61
|
+
**config_kwargs,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
self.config.validate()
|
|
65
|
+
OrcaJointPositions.register_joint_names(self.config.joint_ids)
|
|
66
|
+
|
|
67
|
+
self.recorded_positions: dict[str, OrcaJointPositions] = {}
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def _get_joint_positions(self) -> OrcaJointPositions:
|
|
71
|
+
"""Return the current joint configuration."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def _set_joint_positions(self, joint_pos: OrcaJointPositions) -> bool:
|
|
76
|
+
"""Apply a joint-space command. Return ``True`` if the command was applied."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def _coerce_joint_positions(
|
|
80
|
+
self,
|
|
81
|
+
joint_pos: OrcaJointPositions | Mapping[str, float | None] | np.ndarray,
|
|
82
|
+
) -> OrcaJointPositions:
|
|
83
|
+
if isinstance(joint_pos, OrcaJointPositions):
|
|
84
|
+
return joint_pos
|
|
85
|
+
|
|
86
|
+
if isinstance(joint_pos, Mapping):
|
|
87
|
+
return OrcaJointPositions.from_dict(dict(joint_pos))
|
|
88
|
+
|
|
89
|
+
if isinstance(joint_pos, np.ndarray):
|
|
90
|
+
return OrcaJointPositions.from_ndarray(joint_pos, joint_ids=self.config.joint_ids)
|
|
91
|
+
|
|
92
|
+
raise TypeError(
|
|
93
|
+
"joint_pos must be an OrcaJointPositions instance, a mapping, or a 1-D numpy array."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def pose_from_fractions(
|
|
97
|
+
self,
|
|
98
|
+
fractions: Mapping[str, float],
|
|
99
|
+
) -> OrcaJointPositions:
|
|
100
|
+
"""Build a pose by specifying joints as fractions of their ROM, read from config."""
|
|
101
|
+
pose_dict = {joint: 0.0 for joint in self.config.joint_ids}
|
|
102
|
+
pose_dict.update(self.config.neutral_position)
|
|
103
|
+
|
|
104
|
+
for joint, fraction in fractions.items():
|
|
105
|
+
if joint not in self.config.joint_roms_dict:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
joint_min, joint_max = self.config.joint_roms_dict[joint]
|
|
109
|
+
pose_dict[joint] = joint_min + float(fraction) * (joint_max - joint_min)
|
|
110
|
+
|
|
111
|
+
return self.config.clamp_joint_positions(OrcaJointPositions.from_dict(pose_dict))
|
|
112
|
+
|
|
113
|
+
def set_joint_positions(
|
|
114
|
+
self,
|
|
115
|
+
joint_pos: OrcaJointPositions | Mapping[str, float | None] | np.ndarray,
|
|
116
|
+
num_steps: int = 1,
|
|
117
|
+
step_size: float = 1e-2,
|
|
118
|
+
):
|
|
119
|
+
"""Command the hand to a target joint configuration.
|
|
120
|
+
|
|
121
|
+
Positions are clamped to configured ROM bounds before being sent to
|
|
122
|
+
the hardware for increased safety.
|
|
123
|
+
When *num_steps* > 1 the motion is linearly interpolated
|
|
124
|
+
from the current position, with a ``time.sleep(step_size)`` pause
|
|
125
|
+
between each intermediate waypoint.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
joint_pos: Target joint positions as an
|
|
129
|
+
:class:`~orca_core.OrcaJointPosition`, a ``dict``, or a 1-D
|
|
130
|
+
``np.ndarray`` aligned with :attr:`joint_ids`.
|
|
131
|
+
num_steps: Number of interpolation steps. Use ``1`` for an
|
|
132
|
+
immediate move (default). Simulation hands should always use ``1``.
|
|
133
|
+
step_size: Sleep duration in seconds between interpolated steps.
|
|
134
|
+
Ignored when *num_steps* is ``1``.
|
|
135
|
+
"""
|
|
136
|
+
joint_pos = self._coerce_joint_positions(joint_pos)
|
|
137
|
+
joint_pos = self.config.clamp_joint_positions(joint_pos)
|
|
138
|
+
waypoints = self._linear_waypoints_to(joint_pos, num_steps)
|
|
139
|
+
for i, wp in enumerate(waypoints):
|
|
140
|
+
self._set_joint_positions(wp)
|
|
141
|
+
if i < len(waypoints) - 1:
|
|
142
|
+
time.sleep(step_size)
|
|
143
|
+
|
|
144
|
+
def get_joint_position(self) -> OrcaJointPositions:
|
|
145
|
+
return self._get_joint_positions()
|
|
146
|
+
|
|
147
|
+
def _linear_waypoints_to(
|
|
148
|
+
self, target: OrcaJointPositions, num_steps: int
|
|
149
|
+
) -> list[OrcaJointPositions]:
|
|
150
|
+
"""Return linear waypoints from current pose to *target*."""
|
|
151
|
+
# TODO(fracapuano): Move this to a stateless util function
|
|
152
|
+
if num_steps <= 1:
|
|
153
|
+
return [target]
|
|
154
|
+
|
|
155
|
+
current = self._get_joint_positions()
|
|
156
|
+
out: list[OrcaJointPositions] = []
|
|
157
|
+
for step in range(num_steps + 1):
|
|
158
|
+
t = step / num_steps
|
|
159
|
+
out.append(
|
|
160
|
+
OrcaJointPositions.from_dict({
|
|
161
|
+
joint: current.data[joint] * (1 - t)
|
|
162
|
+
+ target.data.get(joint, current.data[joint]) * t
|
|
163
|
+
for joint in current.data
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
def register_position(self, name: str, joint_pos: OrcaJointPositions):
|
|
169
|
+
self.recorded_positions[name] = self.config.clamp_joint_positions(joint_pos)
|
|
170
|
+
|
|
171
|
+
def remove_position(self, name: str):
|
|
172
|
+
try:
|
|
173
|
+
del self.recorded_positions[name]
|
|
174
|
+
except KeyError:
|
|
175
|
+
pass # position was not among recorded positions
|
|
176
|
+
|
|
177
|
+
def set_named_position(self, name: str, num_steps: int = 1, step_size: float = 1.0):
|
|
178
|
+
self.set_joint_positions(self.recorded_positions[name], num_steps=num_steps, step_size=step_size)
|
|
179
|
+
|
|
180
|
+
def play_named_positions(
|
|
181
|
+
self,
|
|
182
|
+
names: Sequence[str],
|
|
183
|
+
cycles: int = 1,
|
|
184
|
+
num_steps: int = 1,
|
|
185
|
+
step_size: float = 1.0,
|
|
186
|
+
return_to_neutral: bool = False,
|
|
187
|
+
neutral_num_steps: int | None = None,
|
|
188
|
+
neutral_step_size: float | None = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
if cycles < 0:
|
|
191
|
+
raise ValueError("cycles must be non-negative.")
|
|
192
|
+
|
|
193
|
+
for _ in range(cycles):
|
|
194
|
+
for name in names:
|
|
195
|
+
self.set_named_position(name, num_steps=num_steps, step_size=step_size)
|
|
196
|
+
if return_to_neutral:
|
|
197
|
+
self.set_neutral_position(
|
|
198
|
+
num_steps=num_steps if neutral_num_steps is None else neutral_num_steps,
|
|
199
|
+
step_size=step_size if neutral_step_size is None else neutral_step_size,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def run_demo(
|
|
203
|
+
self,
|
|
204
|
+
demo_name: str = "main",
|
|
205
|
+
cycles: int = 1,
|
|
206
|
+
num_steps: int = NUM_STEPS,
|
|
207
|
+
step_size: float = STEP_SIZE,
|
|
208
|
+
return_to_neutral: bool = True,
|
|
209
|
+
) -> tuple[str, ...]:
|
|
210
|
+
if demo_name not in DEMO_POSE_FRACTIONS or demo_name not in DEMO_SEQUENCES:
|
|
211
|
+
available = ", ".join(sorted(DEMO_SEQUENCES))
|
|
212
|
+
raise ValueError(f"Unknown demo '{demo_name}'. Available demos: {available}.")
|
|
213
|
+
|
|
214
|
+
poses = {
|
|
215
|
+
name: self.pose_from_fractions(fractions)
|
|
216
|
+
for name, fractions in DEMO_POSE_FRACTIONS[demo_name].items()
|
|
217
|
+
}
|
|
218
|
+
sequence = DEMO_SEQUENCES[demo_name]
|
|
219
|
+
|
|
220
|
+
for _ in range(cycles):
|
|
221
|
+
for name in sequence:
|
|
222
|
+
self.set_joint_positions(poses[name], num_steps=num_steps, step_size=step_size)
|
|
223
|
+
|
|
224
|
+
if return_to_neutral:
|
|
225
|
+
self.set_neutral_position(num_steps=num_steps, step_size=step_size)
|
|
226
|
+
|
|
227
|
+
return sequence
|
|
228
|
+
|
|
229
|
+
def set_neutral_position(self, num_steps: int = NUM_STEPS, step_size: float = STEP_SIZE):
|
|
230
|
+
"""Move hand to neutral position."""
|
|
231
|
+
self.set_joint_positions(
|
|
232
|
+
OrcaJointPositions.from_dict(self.config.neutral_position),
|
|
233
|
+
num_steps=num_steps,
|
|
234
|
+
step_size=step_size,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def set_zero_position(self, num_steps: int = NUM_STEPS, step_size: float = STEP_SIZE):
|
|
238
|
+
self.set_joint_positions(
|
|
239
|
+
OrcaJointPositions.from_dict(
|
|
240
|
+
{joint: 0.0 for joint in self.config.joint_ids}
|
|
241
|
+
),
|
|
242
|
+
num_steps=num_steps,
|
|
243
|
+
step_size=step_size,
|
|
244
|
+
)
|