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.
Files changed (38) hide show
  1. orca_core/__init__.py +28 -0
  2. orca_core/api/__init__.py +0 -0
  3. orca_core/api/api.py +365 -0
  4. orca_core/base_hand.py +244 -0
  5. orca_core/calibration.py +78 -0
  6. orca_core/constants.py +76 -0
  7. orca_core/core.py +11 -0
  8. orca_core/demo_presets.py +99 -0
  9. orca_core/hand_config.py +402 -0
  10. orca_core/hardware/__init__.py +0 -0
  11. orca_core/hardware/dynamixel_client.py +911 -0
  12. orca_core/hardware/feetech/__init__.py +10 -0
  13. orca_core/hardware/feetech/group_sync_read.py +151 -0
  14. orca_core/hardware/feetech/group_sync_write.py +73 -0
  15. orca_core/hardware/feetech/hls.py +114 -0
  16. orca_core/hardware/feetech/port_handler.py +115 -0
  17. orca_core/hardware/feetech/protocol_packet_handler.py +565 -0
  18. orca_core/hardware/feetech/scscl.py +104 -0
  19. orca_core/hardware/feetech/scservo_def.py +27 -0
  20. orca_core/hardware/feetech/sms_sts.py +114 -0
  21. orca_core/hardware/feetech_client.py +778 -0
  22. orca_core/hardware/mock_dynamixel_client.py +522 -0
  23. orca_core/hardware/mock_tactile_client.py +283 -0
  24. orca_core/hardware/motor_client.py +172 -0
  25. orca_core/hardware/sensing/__init__.py +0 -0
  26. orca_core/hardware/sensing/constants.py +108 -0
  27. orca_core/hardware/sensing/protocol.py +544 -0
  28. orca_core/hardware/sensing/types.py +74 -0
  29. orca_core/hardware/tactile_client.py +775 -0
  30. orca_core/hardware_hand.py +1513 -0
  31. orca_core/joint_position.py +147 -0
  32. orca_core/utils/__init__.py +10 -0
  33. orca_core/utils/utils.py +338 -0
  34. orca_core/version.py +1 -0
  35. orca_core-0.3.0.dist-info/METADATA +114 -0
  36. orca_core-0.3.0.dist-info/RECORD +38 -0
  37. orca_core-0.3.0.dist-info/WHEEL +4 -0
  38. 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
+ )