foodforthought-cli 0.2.4__py3-none-any.whl → 0.2.8__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.
- ate/__init__.py +1 -1
- ate/behaviors/__init__.py +88 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +610 -54
- ate/drivers/__init__.py +27 -0
- ate/drivers/mechdog.py +606 -0
- ate/interfaces/__init__.py +171 -0
- ate/interfaces/base.py +271 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/types.py +371 -0
- ate/mcp_server.py +387 -0
- ate/recording/__init__.py +44 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +405 -0
- ate/recording/upload.py +304 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +79 -0
- ate/robot/calibration.py +583 -0
- ate/robot/commands.py +3603 -0
- ate/robot/discovery.py +339 -0
- ate/robot/introspection.py +330 -0
- ate/robot/manager.py +270 -0
- ate/robot/profiles.py +275 -0
- ate/robot/registry.py +319 -0
- ate/robot/skill_upload.py +393 -0
- ate/robot/visual_labeler.py +1039 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +37 -8
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FoodforThought Robot Interfaces
|
|
3
|
+
|
|
4
|
+
Abstract interfaces that define capabilities for any robot.
|
|
5
|
+
Implementations are hardware-specific, but skills are written against these interfaces.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
RobotInterface (base)
|
|
9
|
+
├── Capabilities (mixins)
|
|
10
|
+
│ ├── Locomotion
|
|
11
|
+
│ │ ├── QuadrupedLocomotion
|
|
12
|
+
│ │ ├── BipedLocomotion
|
|
13
|
+
│ │ ├── WheeledLocomotion
|
|
14
|
+
│ │ └── AerialLocomotion
|
|
15
|
+
│ ├── Manipulation
|
|
16
|
+
│ │ ├── ArmInterface
|
|
17
|
+
│ │ └── GripperInterface
|
|
18
|
+
│ ├── Perception
|
|
19
|
+
│ │ ├── CameraInterface
|
|
20
|
+
│ │ ├── DepthCameraInterface
|
|
21
|
+
│ │ ├── LidarInterface
|
|
22
|
+
│ │ └── IMUInterface
|
|
23
|
+
│ └── Body
|
|
24
|
+
│ └── BodyPoseInterface
|
|
25
|
+
└── Safety
|
|
26
|
+
└── SafetyInterface
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
# MechDog implements:
|
|
30
|
+
class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface):
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
# Spot with arm implements:
|
|
34
|
+
class SpotDriver(QuadrupedLocomotion, BodyPoseInterface, ArmInterface,
|
|
35
|
+
GripperInterface, CameraInterface, LidarInterface, SafetyInterface):
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
# Skills are written against interfaces:
|
|
39
|
+
def pick_and_place(robot: QuadrupedLocomotion & GripperInterface, target: Vector3):
|
|
40
|
+
robot.walk_to(target)
|
|
41
|
+
robot.lower_body(height=0.1)
|
|
42
|
+
robot.grasp()
|
|
43
|
+
...
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from .types import (
|
|
47
|
+
Vector3,
|
|
48
|
+
Quaternion,
|
|
49
|
+
Pose,
|
|
50
|
+
Twist,
|
|
51
|
+
JointState,
|
|
52
|
+
JointLimits,
|
|
53
|
+
Image,
|
|
54
|
+
DepthImage,
|
|
55
|
+
PointCloud,
|
|
56
|
+
IMUReading,
|
|
57
|
+
ForceTorqueReading,
|
|
58
|
+
BatteryState,
|
|
59
|
+
RobotStatus,
|
|
60
|
+
GaitType,
|
|
61
|
+
GripperState,
|
|
62
|
+
GripperStatus,
|
|
63
|
+
ActionResult,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
from .base import (
|
|
67
|
+
RobotInterface,
|
|
68
|
+
SafetyInterface,
|
|
69
|
+
Capability,
|
|
70
|
+
RobotInfo,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
from .locomotion import (
|
|
74
|
+
LocomotionInterface,
|
|
75
|
+
QuadrupedLocomotion,
|
|
76
|
+
BipedLocomotion,
|
|
77
|
+
WheeledLocomotion,
|
|
78
|
+
AerialLocomotion,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
from .manipulation import (
|
|
82
|
+
ArmInterface,
|
|
83
|
+
GripperInterface,
|
|
84
|
+
DualArmInterface,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
from .perception import (
|
|
88
|
+
CameraInterface,
|
|
89
|
+
DepthCameraInterface,
|
|
90
|
+
LidarInterface,
|
|
91
|
+
IMUInterface,
|
|
92
|
+
ForceTorqueInterface,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
from .body import (
|
|
96
|
+
BodyPoseInterface,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
from .detection import (
|
|
100
|
+
BoundingBox,
|
|
101
|
+
Detection,
|
|
102
|
+
DetectionResult,
|
|
103
|
+
ObjectDetectionInterface,
|
|
104
|
+
TrashDetectionInterface,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
from .navigation import (
|
|
108
|
+
NavigationState,
|
|
109
|
+
NavigationGoal,
|
|
110
|
+
NavigationStatus,
|
|
111
|
+
Waypoint,
|
|
112
|
+
NavigationInterface,
|
|
113
|
+
SimpleNavigationInterface,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
__all__ = [
|
|
117
|
+
# Types
|
|
118
|
+
"Vector3",
|
|
119
|
+
"Quaternion",
|
|
120
|
+
"Pose",
|
|
121
|
+
"Twist",
|
|
122
|
+
"JointState",
|
|
123
|
+
"JointLimits",
|
|
124
|
+
"Image",
|
|
125
|
+
"DepthImage",
|
|
126
|
+
"PointCloud",
|
|
127
|
+
"IMUReading",
|
|
128
|
+
"ForceTorqueReading",
|
|
129
|
+
"BatteryState",
|
|
130
|
+
"RobotStatus",
|
|
131
|
+
"GaitType",
|
|
132
|
+
"GripperState",
|
|
133
|
+
"GripperStatus",
|
|
134
|
+
"ActionResult",
|
|
135
|
+
# Base
|
|
136
|
+
"RobotInterface",
|
|
137
|
+
"SafetyInterface",
|
|
138
|
+
"Capability",
|
|
139
|
+
"RobotInfo",
|
|
140
|
+
# Locomotion
|
|
141
|
+
"LocomotionInterface",
|
|
142
|
+
"QuadrupedLocomotion",
|
|
143
|
+
"BipedLocomotion",
|
|
144
|
+
"WheeledLocomotion",
|
|
145
|
+
"AerialLocomotion",
|
|
146
|
+
# Manipulation
|
|
147
|
+
"ArmInterface",
|
|
148
|
+
"GripperInterface",
|
|
149
|
+
"DualArmInterface",
|
|
150
|
+
# Perception
|
|
151
|
+
"CameraInterface",
|
|
152
|
+
"DepthCameraInterface",
|
|
153
|
+
"LidarInterface",
|
|
154
|
+
"IMUInterface",
|
|
155
|
+
"ForceTorqueInterface",
|
|
156
|
+
# Body
|
|
157
|
+
"BodyPoseInterface",
|
|
158
|
+
# Detection (higher-level)
|
|
159
|
+
"BoundingBox",
|
|
160
|
+
"Detection",
|
|
161
|
+
"DetectionResult",
|
|
162
|
+
"ObjectDetectionInterface",
|
|
163
|
+
"TrashDetectionInterface",
|
|
164
|
+
# Navigation (higher-level)
|
|
165
|
+
"NavigationState",
|
|
166
|
+
"NavigationGoal",
|
|
167
|
+
"NavigationStatus",
|
|
168
|
+
"Waypoint",
|
|
169
|
+
"NavigationInterface",
|
|
170
|
+
"SimpleNavigationInterface",
|
|
171
|
+
]
|
ate/interfaces/base.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interfaces that all robots must implement.
|
|
3
|
+
|
|
4
|
+
Every robot, regardless of type, has:
|
|
5
|
+
1. Identity - what it is, what it can do
|
|
6
|
+
2. Safety - emergency stop, status monitoring
|
|
7
|
+
3. Lifecycle - connect, disconnect, ready state
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import List, Optional, Set, Type, TYPE_CHECKING
|
|
13
|
+
from enum import Enum, auto
|
|
14
|
+
|
|
15
|
+
from .types import RobotStatus, BatteryState, ActionResult
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .locomotion import LocomotionInterface
|
|
19
|
+
from .manipulation import ArmInterface, GripperInterface
|
|
20
|
+
from .perception import CameraInterface, LidarInterface, IMUInterface
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Capability(Enum):
|
|
24
|
+
"""
|
|
25
|
+
Capabilities a robot can have.
|
|
26
|
+
Used for runtime capability checking and skill compatibility.
|
|
27
|
+
"""
|
|
28
|
+
# Locomotion
|
|
29
|
+
QUADRUPED = auto()
|
|
30
|
+
BIPED = auto()
|
|
31
|
+
WHEELED = auto()
|
|
32
|
+
AERIAL = auto()
|
|
33
|
+
AQUATIC = auto()
|
|
34
|
+
|
|
35
|
+
# Manipulation
|
|
36
|
+
ARM = auto()
|
|
37
|
+
DUAL_ARM = auto()
|
|
38
|
+
GRIPPER = auto()
|
|
39
|
+
SUCTION = auto()
|
|
40
|
+
|
|
41
|
+
# Perception
|
|
42
|
+
CAMERA = auto()
|
|
43
|
+
DEPTH_CAMERA = auto()
|
|
44
|
+
LIDAR = auto()
|
|
45
|
+
IMU = auto()
|
|
46
|
+
FORCE_TORQUE = auto()
|
|
47
|
+
TACTILE = auto()
|
|
48
|
+
|
|
49
|
+
# Body control
|
|
50
|
+
BODY_POSE = auto()
|
|
51
|
+
|
|
52
|
+
# Communication
|
|
53
|
+
AUDIO_INPUT = auto()
|
|
54
|
+
AUDIO_OUTPUT = auto()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class RobotInfo:
|
|
59
|
+
"""
|
|
60
|
+
Static information about a robot.
|
|
61
|
+
Used for compatibility checking and UI display.
|
|
62
|
+
"""
|
|
63
|
+
# Identity
|
|
64
|
+
name: str # Human-readable name
|
|
65
|
+
model: str # Model identifier (e.g., "hiwonder_mechdog")
|
|
66
|
+
manufacturer: str # Manufacturer name
|
|
67
|
+
archetype: str # Primary type: "quadruped", "humanoid", "arm", etc.
|
|
68
|
+
|
|
69
|
+
# Capabilities
|
|
70
|
+
capabilities: Set[Capability] = field(default_factory=set)
|
|
71
|
+
|
|
72
|
+
# Physical properties
|
|
73
|
+
mass: Optional[float] = None # kg
|
|
74
|
+
payload_capacity: Optional[float] = None # kg
|
|
75
|
+
reach: Optional[float] = None # meters (for arms)
|
|
76
|
+
dimensions: Optional[tuple] = None # (length, width, height) in meters
|
|
77
|
+
|
|
78
|
+
# Workspace limits (in base frame)
|
|
79
|
+
workspace_min: Optional[tuple] = None # (x, y, z) min bounds
|
|
80
|
+
workspace_max: Optional[tuple] = None # (x, y, z) max bounds
|
|
81
|
+
|
|
82
|
+
# Performance
|
|
83
|
+
max_speed: Optional[float] = None # m/s
|
|
84
|
+
battery_capacity: Optional[float] = None # Wh
|
|
85
|
+
|
|
86
|
+
# Metadata
|
|
87
|
+
firmware_version: Optional[str] = None
|
|
88
|
+
serial_number: Optional[str] = None
|
|
89
|
+
description: str = ""
|
|
90
|
+
|
|
91
|
+
def has_capability(self, cap: Capability) -> bool:
|
|
92
|
+
return cap in self.capabilities
|
|
93
|
+
|
|
94
|
+
def has_all_capabilities(self, caps: Set[Capability]) -> bool:
|
|
95
|
+
return caps.issubset(self.capabilities)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RobotInterface(ABC):
|
|
99
|
+
"""
|
|
100
|
+
Base interface that ALL robots must implement.
|
|
101
|
+
|
|
102
|
+
This provides:
|
|
103
|
+
- Identity and capability information
|
|
104
|
+
- Connection lifecycle
|
|
105
|
+
- Status monitoring
|
|
106
|
+
|
|
107
|
+
Specific capabilities (locomotion, manipulation, etc.) are added via mixins.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def get_info(self) -> RobotInfo:
|
|
112
|
+
"""
|
|
113
|
+
Get static information about this robot.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
RobotInfo with name, model, capabilities, etc.
|
|
117
|
+
"""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def connect(self) -> ActionResult:
|
|
122
|
+
"""
|
|
123
|
+
Establish connection to the robot hardware.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
ActionResult indicating success/failure
|
|
127
|
+
"""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def disconnect(self) -> ActionResult:
|
|
132
|
+
"""
|
|
133
|
+
Safely disconnect from the robot hardware.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
ActionResult indicating success/failure
|
|
137
|
+
"""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def is_connected(self) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Check if currently connected to the robot.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if connected and communication is working
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
def get_status(self) -> RobotStatus:
|
|
152
|
+
"""
|
|
153
|
+
Get current robot status.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
RobotStatus with mode, errors, battery, etc.
|
|
157
|
+
"""
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
def has_capability(self, cap: Capability) -> bool:
|
|
161
|
+
"""Check if robot has a specific capability."""
|
|
162
|
+
return self.get_info().has_capability(cap)
|
|
163
|
+
|
|
164
|
+
def get_capabilities(self) -> Set[Capability]:
|
|
165
|
+
"""Get all capabilities of this robot."""
|
|
166
|
+
return self.get_info().capabilities
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SafetyInterface(ABC):
|
|
170
|
+
"""
|
|
171
|
+
Safety interface that all robots should implement.
|
|
172
|
+
|
|
173
|
+
Provides emergency stop and safety monitoring.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
def emergency_stop(self) -> ActionResult:
|
|
178
|
+
"""
|
|
179
|
+
Immediately stop all motion and enter safe state.
|
|
180
|
+
|
|
181
|
+
This should:
|
|
182
|
+
- Stop all actuators immediately
|
|
183
|
+
- Disable motor power if possible
|
|
184
|
+
- Set robot to ESTOPPED mode
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
ActionResult (should rarely fail)
|
|
188
|
+
"""
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def reset_emergency_stop(self) -> ActionResult:
|
|
193
|
+
"""
|
|
194
|
+
Clear emergency stop state and return to ready.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
ActionResult indicating if reset was successful
|
|
198
|
+
"""
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
@abstractmethod
|
|
202
|
+
def is_estopped(self) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Check if emergency stop is active.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if robot is in emergency stop state
|
|
208
|
+
"""
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def get_battery_state(self) -> Optional[BatteryState]:
|
|
213
|
+
"""
|
|
214
|
+
Get current battery status.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
BatteryState or None if no battery
|
|
218
|
+
"""
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
def check_safety(self) -> List[str]:
|
|
222
|
+
"""
|
|
223
|
+
Run safety checks and return list of issues.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List of safety warning/error messages (empty if all OK)
|
|
227
|
+
"""
|
|
228
|
+
issues = []
|
|
229
|
+
|
|
230
|
+
battery = self.get_battery_state()
|
|
231
|
+
if battery:
|
|
232
|
+
if battery.percentage < 0.1:
|
|
233
|
+
issues.append(f"Critical battery: {battery.percentage*100:.0f}%")
|
|
234
|
+
elif battery.percentage < 0.2:
|
|
235
|
+
issues.append(f"Low battery: {battery.percentage*100:.0f}%")
|
|
236
|
+
|
|
237
|
+
if self.is_estopped():
|
|
238
|
+
issues.append("Emergency stop is active")
|
|
239
|
+
|
|
240
|
+
return issues
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# =============================================================================
|
|
244
|
+
# Utility functions for capability checking
|
|
245
|
+
# =============================================================================
|
|
246
|
+
|
|
247
|
+
def requires_capabilities(*caps: Capability):
|
|
248
|
+
"""
|
|
249
|
+
Decorator to mark that a skill requires certain capabilities.
|
|
250
|
+
|
|
251
|
+
Usage:
|
|
252
|
+
@requires_capabilities(Capability.QUADRUPED, Capability.GRIPPER)
|
|
253
|
+
def pick_up_object(robot, target):
|
|
254
|
+
...
|
|
255
|
+
"""
|
|
256
|
+
def decorator(func):
|
|
257
|
+
func._required_capabilities = set(caps)
|
|
258
|
+
return func
|
|
259
|
+
return decorator
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def check_robot_compatibility(robot: RobotInterface, required: Set[Capability]) -> List[str]:
|
|
263
|
+
"""
|
|
264
|
+
Check if a robot is compatible with required capabilities.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of missing capabilities (empty if compatible)
|
|
268
|
+
"""
|
|
269
|
+
robot_caps = robot.get_capabilities()
|
|
270
|
+
missing = required - robot_caps
|
|
271
|
+
return [cap.name for cap in missing]
|
ate/interfaces/body.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Body pose interfaces for robots with adjustable body position/orientation.
|
|
3
|
+
|
|
4
|
+
Primarily for legged robots that can:
|
|
5
|
+
- Adjust body height
|
|
6
|
+
- Tilt body (roll, pitch, yaw)
|
|
7
|
+
- Shift body position relative to feet
|
|
8
|
+
|
|
9
|
+
These are independent of locomotion - you can adjust pose while standing still.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from .types import (
|
|
16
|
+
Vector3,
|
|
17
|
+
Quaternion,
|
|
18
|
+
Pose,
|
|
19
|
+
ActionResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BodyPoseInterface(ABC):
|
|
24
|
+
"""
|
|
25
|
+
Interface for controlling robot body pose.
|
|
26
|
+
|
|
27
|
+
Implemented by quadrupeds, bipeds, and other robots that can adjust
|
|
28
|
+
their body position/orientation independent of locomotion.
|
|
29
|
+
|
|
30
|
+
Coordinate frame:
|
|
31
|
+
- Body origin is typically at the center of the robot
|
|
32
|
+
- Z+ is up, X+ is forward, Y+ is left
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# =========================================================================
|
|
36
|
+
# Body height control
|
|
37
|
+
# =========================================================================
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def set_body_height(self, height: float) -> ActionResult:
|
|
41
|
+
"""
|
|
42
|
+
Set body height above ground.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
height: Height in meters (from nominal ground level)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
ActionResult
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def get_body_height(self) -> float:
|
|
54
|
+
"""
|
|
55
|
+
Get current body height.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Height in meters
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def get_height_limits(self) -> Tuple[float, float]:
|
|
63
|
+
"""
|
|
64
|
+
Get allowed body height range.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
(min_height, max_height) in meters
|
|
68
|
+
"""
|
|
69
|
+
return (0.05, 0.30) # Default for small quadrupeds
|
|
70
|
+
|
|
71
|
+
# =========================================================================
|
|
72
|
+
# Body orientation control
|
|
73
|
+
# =========================================================================
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def set_body_orientation(
|
|
77
|
+
self,
|
|
78
|
+
roll: float = 0.0,
|
|
79
|
+
pitch: float = 0.0,
|
|
80
|
+
yaw: float = 0.0
|
|
81
|
+
) -> ActionResult:
|
|
82
|
+
"""
|
|
83
|
+
Set body orientation (tilt).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
roll: Roll angle in radians (positive = right side down)
|
|
87
|
+
pitch: Pitch angle in radians (positive = nose down)
|
|
88
|
+
yaw: Yaw angle in radians (positive = turn left)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
ActionResult
|
|
92
|
+
"""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def get_body_orientation(self) -> Tuple[float, float, float]:
|
|
97
|
+
"""
|
|
98
|
+
Get current body orientation.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
(roll, pitch, yaw) in radians
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def get_orientation_limits(self) -> Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]:
|
|
106
|
+
"""
|
|
107
|
+
Get allowed orientation ranges.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
((roll_min, roll_max), (pitch_min, pitch_max), (yaw_min, yaw_max))
|
|
111
|
+
All in radians
|
|
112
|
+
"""
|
|
113
|
+
import math
|
|
114
|
+
return (
|
|
115
|
+
(-math.pi/6, math.pi/6), # ±30° roll
|
|
116
|
+
(-math.pi/6, math.pi/6), # ±30° pitch
|
|
117
|
+
(-math.pi/4, math.pi/4), # ±45° yaw
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# =========================================================================
|
|
121
|
+
# Body position control
|
|
122
|
+
# =========================================================================
|
|
123
|
+
|
|
124
|
+
def set_body_position(self, offset: Vector3) -> ActionResult:
|
|
125
|
+
"""
|
|
126
|
+
Shift body position relative to feet.
|
|
127
|
+
|
|
128
|
+
This moves the body while keeping feet planted.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
offset: Position offset from center (x=forward, y=left, z=up)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
ActionResult
|
|
135
|
+
"""
|
|
136
|
+
return ActionResult.error("Body position shift not supported")
|
|
137
|
+
|
|
138
|
+
def get_body_position(self) -> Vector3:
|
|
139
|
+
"""
|
|
140
|
+
Get current body position offset.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Vector3 offset from center
|
|
144
|
+
"""
|
|
145
|
+
return Vector3.zero()
|
|
146
|
+
|
|
147
|
+
# =========================================================================
|
|
148
|
+
# Combined pose control
|
|
149
|
+
# =========================================================================
|
|
150
|
+
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def set_body_pose(
|
|
153
|
+
self,
|
|
154
|
+
height: Optional[float] = None,
|
|
155
|
+
roll: Optional[float] = None,
|
|
156
|
+
pitch: Optional[float] = None,
|
|
157
|
+
yaw: Optional[float] = None,
|
|
158
|
+
x_offset: Optional[float] = None,
|
|
159
|
+
y_offset: Optional[float] = None
|
|
160
|
+
) -> ActionResult:
|
|
161
|
+
"""
|
|
162
|
+
Set multiple body pose parameters at once.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
height: Body height (None = keep current)
|
|
166
|
+
roll, pitch, yaw: Orientation (None = keep current)
|
|
167
|
+
x_offset, y_offset: Body position offset (None = keep current)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
ActionResult
|
|
171
|
+
"""
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def get_body_pose(self) -> dict:
|
|
175
|
+
"""
|
|
176
|
+
Get complete body pose.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dict with height, roll, pitch, yaw, x_offset, y_offset
|
|
180
|
+
"""
|
|
181
|
+
roll, pitch, yaw = self.get_body_orientation()
|
|
182
|
+
pos = self.get_body_position()
|
|
183
|
+
return {
|
|
184
|
+
"height": self.get_body_height(),
|
|
185
|
+
"roll": roll,
|
|
186
|
+
"pitch": pitch,
|
|
187
|
+
"yaw": yaw,
|
|
188
|
+
"x_offset": pos.x,
|
|
189
|
+
"y_offset": pos.y,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# =========================================================================
|
|
193
|
+
# Preset poses
|
|
194
|
+
# =========================================================================
|
|
195
|
+
|
|
196
|
+
def reset_pose(self) -> ActionResult:
|
|
197
|
+
"""
|
|
198
|
+
Reset to default/neutral body pose.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
ActionResult
|
|
202
|
+
"""
|
|
203
|
+
height_min, height_max = self.get_height_limits()
|
|
204
|
+
default_height = (height_min + height_max) / 2
|
|
205
|
+
return self.set_body_pose(
|
|
206
|
+
height=default_height,
|
|
207
|
+
roll=0.0,
|
|
208
|
+
pitch=0.0,
|
|
209
|
+
yaw=0.0,
|
|
210
|
+
x_offset=0.0,
|
|
211
|
+
y_offset=0.0
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def lower_body(self, height: Optional[float] = None) -> ActionResult:
|
|
215
|
+
"""
|
|
216
|
+
Lower body (for grasping objects on ground).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
height: Target height (None = minimum safe height)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
ActionResult
|
|
223
|
+
"""
|
|
224
|
+
if height is None:
|
|
225
|
+
height = self.get_height_limits()[0] + 0.02 # 2cm above min
|
|
226
|
+
return self.set_body_height(height)
|
|
227
|
+
|
|
228
|
+
def raise_body(self, height: Optional[float] = None) -> ActionResult:
|
|
229
|
+
"""
|
|
230
|
+
Raise body to maximum comfortable height.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
height: Target height (None = default standing height)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
ActionResult
|
|
237
|
+
"""
|
|
238
|
+
if height is None:
|
|
239
|
+
height_min, height_max = self.get_height_limits()
|
|
240
|
+
height = (height_min + height_max) / 2
|
|
241
|
+
return self.set_body_height(height)
|
|
242
|
+
|
|
243
|
+
def look_at(self, direction: Vector3) -> ActionResult:
|
|
244
|
+
"""
|
|
245
|
+
Tilt body to "look" in a direction.
|
|
246
|
+
|
|
247
|
+
Adjusts pitch and yaw to point forward axis toward direction.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
direction: Direction to look (in robot base frame)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
ActionResult
|
|
254
|
+
"""
|
|
255
|
+
import math
|
|
256
|
+
|
|
257
|
+
# Calculate required yaw and pitch
|
|
258
|
+
yaw = math.atan2(direction.y, direction.x)
|
|
259
|
+
horizontal_dist = math.sqrt(direction.x**2 + direction.y**2)
|
|
260
|
+
pitch = -math.atan2(direction.z, horizontal_dist)
|
|
261
|
+
|
|
262
|
+
# Clamp to limits
|
|
263
|
+
roll_lim, pitch_lim, yaw_lim = self.get_orientation_limits()
|
|
264
|
+
pitch = max(pitch_lim[0], min(pitch_lim[1], pitch))
|
|
265
|
+
yaw = max(yaw_lim[0], min(yaw_lim[1], yaw))
|
|
266
|
+
|
|
267
|
+
return self.set_body_orientation(roll=0.0, pitch=pitch, yaw=yaw)
|