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
ate/drivers/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hardware drivers for specific robots.
|
|
3
|
+
|
|
4
|
+
Each driver implements the appropriate interfaces from ate.interfaces.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from ate.drivers import MechDogDriver
|
|
8
|
+
|
|
9
|
+
# Connect to MechDog
|
|
10
|
+
dog = MechDogDriver(port="/dev/cu.usbserial-10")
|
|
11
|
+
dog.connect()
|
|
12
|
+
|
|
13
|
+
# Use through abstract interface
|
|
14
|
+
dog.stand()
|
|
15
|
+
dog.walk(Vector3.forward(), speed=0.3)
|
|
16
|
+
dog.set_body_height(0.15)
|
|
17
|
+
dog.stop()
|
|
18
|
+
|
|
19
|
+
# Disconnect
|
|
20
|
+
dog.disconnect()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .mechdog import MechDogDriver
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"MechDogDriver",
|
|
27
|
+
]
|
ate/drivers/mechdog.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MechDog driver implementation.
|
|
3
|
+
|
|
4
|
+
Implements the FoodforThought interfaces for HiWonder MechDog.
|
|
5
|
+
|
|
6
|
+
The MechDog runs MicroPython with the HW_MechDog library.
|
|
7
|
+
Communication is via serial REPL commands.
|
|
8
|
+
|
|
9
|
+
Hardware specs:
|
|
10
|
+
- 12 servos (3 per leg: hip, thigh, calf)
|
|
11
|
+
- Optional arm attachment (3 servos)
|
|
12
|
+
- ESP32-based controller
|
|
13
|
+
- LX-16A serial bus servos
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import math
|
|
18
|
+
from typing import Optional, List, Set, Tuple
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import serial
|
|
23
|
+
HAS_SERIAL = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
HAS_SERIAL = False
|
|
26
|
+
|
|
27
|
+
from ..interfaces.types import (
|
|
28
|
+
Vector3,
|
|
29
|
+
Quaternion,
|
|
30
|
+
Pose,
|
|
31
|
+
Twist,
|
|
32
|
+
JointState,
|
|
33
|
+
GaitType,
|
|
34
|
+
ActionResult,
|
|
35
|
+
RobotStatus,
|
|
36
|
+
RobotMode,
|
|
37
|
+
BatteryState,
|
|
38
|
+
IMUReading,
|
|
39
|
+
)
|
|
40
|
+
from ..interfaces.base import (
|
|
41
|
+
RobotInterface,
|
|
42
|
+
SafetyInterface,
|
|
43
|
+
RobotInfo,
|
|
44
|
+
Capability,
|
|
45
|
+
)
|
|
46
|
+
from ..interfaces.locomotion import QuadrupedLocomotion
|
|
47
|
+
from ..interfaces.body import BodyPoseInterface
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# MechDog-specific constants
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
# Servo value ranges (raw values from MechDog)
|
|
55
|
+
SERVO_MIN = 0
|
|
56
|
+
SERVO_MAX = 4096
|
|
57
|
+
SERVO_CENTER = 2048
|
|
58
|
+
|
|
59
|
+
# Body dimensions (meters)
|
|
60
|
+
BODY_LENGTH = 0.20 # Front to back
|
|
61
|
+
BODY_WIDTH = 0.10 # Side to side
|
|
62
|
+
LEG_LENGTH = 0.12 # Total leg length
|
|
63
|
+
|
|
64
|
+
# Height limits (meters)
|
|
65
|
+
HEIGHT_MIN = 0.05
|
|
66
|
+
HEIGHT_MAX = 0.18
|
|
67
|
+
HEIGHT_DEFAULT = 0.12
|
|
68
|
+
|
|
69
|
+
# Speed limits
|
|
70
|
+
MAX_STRIDE_MM = 100 # Maximum stride in mm
|
|
71
|
+
MAX_TURN_DEG = 30 # Maximum turn rate in degrees
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class MechDogConfig:
|
|
76
|
+
"""Configuration for MechDog connection."""
|
|
77
|
+
port: str = "/dev/cu.usbserial-10"
|
|
78
|
+
baud_rate: int = 115200
|
|
79
|
+
timeout: float = 2.0
|
|
80
|
+
has_arm: bool = False
|
|
81
|
+
has_camera: bool = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, RobotInterface):
|
|
85
|
+
"""
|
|
86
|
+
Driver for HiWonder MechDog quadruped robot.
|
|
87
|
+
|
|
88
|
+
Implements:
|
|
89
|
+
- QuadrupedLocomotion: walk, turn, stand, sit, etc.
|
|
90
|
+
- BodyPoseInterface: height, roll, pitch, yaw control
|
|
91
|
+
- SafetyInterface: emergency stop, battery monitoring
|
|
92
|
+
- RobotInterface: connection lifecycle, status
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
dog = MechDogDriver(port="/dev/cu.usbserial-10")
|
|
96
|
+
dog.connect()
|
|
97
|
+
|
|
98
|
+
dog.stand()
|
|
99
|
+
dog.walk(Vector3.forward(), speed=0.3)
|
|
100
|
+
dog.set_body_height(0.10)
|
|
101
|
+
|
|
102
|
+
dog.disconnect()
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, port: str = "/dev/cu.usbserial-10", config: Optional[MechDogConfig] = None):
|
|
106
|
+
"""
|
|
107
|
+
Initialize MechDog driver.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
port: Serial port for MechDog connection
|
|
111
|
+
config: Optional configuration
|
|
112
|
+
"""
|
|
113
|
+
self.config = config or MechDogConfig(port=port)
|
|
114
|
+
self._serial: Optional[serial.Serial] = None
|
|
115
|
+
self._connected = False
|
|
116
|
+
self._estopped = False
|
|
117
|
+
self._current_gait = GaitType.WALK
|
|
118
|
+
|
|
119
|
+
# Cached state
|
|
120
|
+
self._current_height = HEIGHT_DEFAULT
|
|
121
|
+
self._current_orientation = (0.0, 0.0, 0.0) # roll, pitch, yaw
|
|
122
|
+
self._is_moving = False
|
|
123
|
+
|
|
124
|
+
# Odometry (basic dead reckoning)
|
|
125
|
+
self._pose = Pose.identity()
|
|
126
|
+
self._velocity = Twist.zero()
|
|
127
|
+
|
|
128
|
+
# =========================================================================
|
|
129
|
+
# RobotInterface implementation
|
|
130
|
+
# =========================================================================
|
|
131
|
+
|
|
132
|
+
def get_info(self) -> RobotInfo:
|
|
133
|
+
"""Get MechDog information."""
|
|
134
|
+
capabilities = {
|
|
135
|
+
Capability.QUADRUPED,
|
|
136
|
+
Capability.BODY_POSE,
|
|
137
|
+
Capability.IMU,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if self.config.has_arm:
|
|
141
|
+
capabilities.add(Capability.ARM)
|
|
142
|
+
capabilities.add(Capability.GRIPPER)
|
|
143
|
+
|
|
144
|
+
if self.config.has_camera:
|
|
145
|
+
capabilities.add(Capability.CAMERA)
|
|
146
|
+
|
|
147
|
+
return RobotInfo(
|
|
148
|
+
name="MechDog",
|
|
149
|
+
model="hiwonder_mechdog",
|
|
150
|
+
manufacturer="HiWonder",
|
|
151
|
+
archetype="quadruped",
|
|
152
|
+
capabilities=capabilities,
|
|
153
|
+
mass=1.5, # kg
|
|
154
|
+
dimensions=(BODY_LENGTH, BODY_WIDTH, HEIGHT_DEFAULT),
|
|
155
|
+
workspace_min=(-2.0, -2.0, 0.0),
|
|
156
|
+
workspace_max=(2.0, 2.0, 0.5),
|
|
157
|
+
max_speed=0.3, # m/s
|
|
158
|
+
description="HiWonder MechDog quadruped robot with 12 DOF",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def connect(self) -> ActionResult:
|
|
162
|
+
"""Connect to MechDog via serial."""
|
|
163
|
+
if not HAS_SERIAL:
|
|
164
|
+
return ActionResult.error("pyserial not installed. Run: pip install pyserial")
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
self._serial = serial.Serial(
|
|
168
|
+
self.config.port,
|
|
169
|
+
self.config.baud_rate,
|
|
170
|
+
timeout=self.config.timeout
|
|
171
|
+
)
|
|
172
|
+
time.sleep(0.5)
|
|
173
|
+
|
|
174
|
+
# Enter friendly REPL mode
|
|
175
|
+
self._serial.write(b'\x03') # Ctrl+C to interrupt
|
|
176
|
+
time.sleep(0.3)
|
|
177
|
+
self._serial.write(b'\x02') # Ctrl+B for friendly REPL
|
|
178
|
+
time.sleep(0.5)
|
|
179
|
+
self._serial.reset_input_buffer()
|
|
180
|
+
|
|
181
|
+
# Initialize MechDog library
|
|
182
|
+
result = self._send_command("from HW_MechDog import MechDog")
|
|
183
|
+
if "Error" in result or "Traceback" in result:
|
|
184
|
+
return ActionResult.error(f"Failed to import HW_MechDog: {result}")
|
|
185
|
+
|
|
186
|
+
result = self._send_command("dog = MechDog()")
|
|
187
|
+
if "Error" in result or "Traceback" in result:
|
|
188
|
+
return ActionResult.error(f"Failed to create MechDog: {result}")
|
|
189
|
+
|
|
190
|
+
self._connected = True
|
|
191
|
+
return ActionResult.ok("Connected to MechDog")
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return ActionResult.error(f"Connection failed: {e}")
|
|
195
|
+
|
|
196
|
+
def disconnect(self) -> ActionResult:
|
|
197
|
+
"""Disconnect from MechDog."""
|
|
198
|
+
if self._serial:
|
|
199
|
+
try:
|
|
200
|
+
# Stop any movement first
|
|
201
|
+
self.stop()
|
|
202
|
+
self._serial.close()
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
finally:
|
|
206
|
+
self._serial = None
|
|
207
|
+
self._connected = False
|
|
208
|
+
|
|
209
|
+
return ActionResult.ok("Disconnected")
|
|
210
|
+
|
|
211
|
+
def is_connected(self) -> bool:
|
|
212
|
+
"""Check connection status."""
|
|
213
|
+
return self._connected and self._serial is not None and self._serial.is_open
|
|
214
|
+
|
|
215
|
+
def get_status(self) -> RobotStatus:
|
|
216
|
+
"""Get robot status."""
|
|
217
|
+
mode = RobotMode.IDLE
|
|
218
|
+
if self._estopped:
|
|
219
|
+
mode = RobotMode.ESTOPPED
|
|
220
|
+
elif self._is_moving:
|
|
221
|
+
mode = RobotMode.MOVING
|
|
222
|
+
elif self._connected:
|
|
223
|
+
mode = RobotMode.READY
|
|
224
|
+
|
|
225
|
+
return RobotStatus(
|
|
226
|
+
mode=mode,
|
|
227
|
+
is_ready=self._connected and not self._estopped,
|
|
228
|
+
is_moving=self._is_moving,
|
|
229
|
+
errors=[],
|
|
230
|
+
battery=self.get_battery_state(),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# =========================================================================
|
|
234
|
+
# SafetyInterface implementation
|
|
235
|
+
# =========================================================================
|
|
236
|
+
|
|
237
|
+
def emergency_stop(self) -> ActionResult:
|
|
238
|
+
"""Emergency stop - immediately stop all motion."""
|
|
239
|
+
self._estopped = True
|
|
240
|
+
self._is_moving = False
|
|
241
|
+
|
|
242
|
+
if self._serial:
|
|
243
|
+
# Send stop command
|
|
244
|
+
self._send_command("dog.move(0, 0)")
|
|
245
|
+
|
|
246
|
+
return ActionResult.ok("Emergency stop activated")
|
|
247
|
+
|
|
248
|
+
def reset_emergency_stop(self) -> ActionResult:
|
|
249
|
+
"""Clear emergency stop."""
|
|
250
|
+
self._estopped = False
|
|
251
|
+
return ActionResult.ok("Emergency stop cleared")
|
|
252
|
+
|
|
253
|
+
def is_estopped(self) -> bool:
|
|
254
|
+
"""Check if estopped."""
|
|
255
|
+
return self._estopped
|
|
256
|
+
|
|
257
|
+
def get_battery_state(self) -> Optional[BatteryState]:
|
|
258
|
+
"""Get battery state (not supported on basic MechDog)."""
|
|
259
|
+
# MechDog doesn't report battery state via REPL
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
# =========================================================================
|
|
263
|
+
# QuadrupedLocomotion implementation
|
|
264
|
+
# =========================================================================
|
|
265
|
+
|
|
266
|
+
def get_pose(self) -> Pose:
|
|
267
|
+
"""Get current pose (from dead reckoning)."""
|
|
268
|
+
return self._pose
|
|
269
|
+
|
|
270
|
+
def get_velocity(self) -> Twist:
|
|
271
|
+
"""Get current velocity."""
|
|
272
|
+
return self._velocity
|
|
273
|
+
|
|
274
|
+
def stop(self) -> ActionResult:
|
|
275
|
+
"""Stop all movement."""
|
|
276
|
+
if not self._check_connection():
|
|
277
|
+
return ActionResult.error("Not connected")
|
|
278
|
+
|
|
279
|
+
self._send_command("dog.move(0, 0)")
|
|
280
|
+
self._is_moving = False
|
|
281
|
+
self._velocity = Twist.zero()
|
|
282
|
+
|
|
283
|
+
return ActionResult.ok("Stopped")
|
|
284
|
+
|
|
285
|
+
def is_moving(self) -> bool:
|
|
286
|
+
"""Check if moving."""
|
|
287
|
+
return self._is_moving
|
|
288
|
+
|
|
289
|
+
def walk(self, direction: Vector3, speed: float = 0.5) -> ActionResult:
|
|
290
|
+
"""Walk in direction at speed."""
|
|
291
|
+
if not self._check_connection():
|
|
292
|
+
return ActionResult.error("Not connected")
|
|
293
|
+
|
|
294
|
+
if self._estopped:
|
|
295
|
+
return ActionResult.error("Emergency stop active")
|
|
296
|
+
|
|
297
|
+
# Convert direction and speed to MechDog stride/angle
|
|
298
|
+
# MechDog uses: move(stride_mm, angle_deg)
|
|
299
|
+
# stride: -100 to +100 (negative = backward)
|
|
300
|
+
# angle: -30 to +30 (negative = turn right)
|
|
301
|
+
|
|
302
|
+
# Calculate stride from speed (simple mapping)
|
|
303
|
+
stride = int(min(MAX_STRIDE_MM, max(-MAX_STRIDE_MM, speed * 200)))
|
|
304
|
+
|
|
305
|
+
# Apply direction
|
|
306
|
+
if direction.x < 0: # Backward
|
|
307
|
+
stride = -abs(stride)
|
|
308
|
+
|
|
309
|
+
# Calculate turn from y component
|
|
310
|
+
angle = int(direction.y * MAX_TURN_DEG)
|
|
311
|
+
angle = max(-MAX_TURN_DEG, min(MAX_TURN_DEG, angle))
|
|
312
|
+
|
|
313
|
+
self._send_command(f"dog.move({stride}, {angle})")
|
|
314
|
+
self._is_moving = True
|
|
315
|
+
|
|
316
|
+
# Update velocity estimate
|
|
317
|
+
self._velocity = Twist(
|
|
318
|
+
linear=Vector3(speed * direction.x, speed * direction.y, 0),
|
|
319
|
+
angular=Vector3.zero()
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return ActionResult.ok(f"Walking: stride={stride}, angle={angle}")
|
|
323
|
+
|
|
324
|
+
def walk_to(self, target: Vector3, speed: float = 0.5) -> ActionResult:
|
|
325
|
+
"""Walk to target position (blocking)."""
|
|
326
|
+
if not self._check_connection():
|
|
327
|
+
return ActionResult.error("Not connected")
|
|
328
|
+
|
|
329
|
+
# Simple implementation: walk toward target
|
|
330
|
+
# In a real implementation, this would use feedback control
|
|
331
|
+
|
|
332
|
+
current = self.get_pose().position
|
|
333
|
+
delta = target - current
|
|
334
|
+
distance = delta.magnitude()
|
|
335
|
+
|
|
336
|
+
if distance < 0.05: # Already at target
|
|
337
|
+
return ActionResult.ok("Already at target")
|
|
338
|
+
|
|
339
|
+
# Calculate direction
|
|
340
|
+
direction = delta.normalized()
|
|
341
|
+
|
|
342
|
+
# Walk toward target
|
|
343
|
+
self.walk(direction, speed)
|
|
344
|
+
|
|
345
|
+
# Wait for approximate time to reach target
|
|
346
|
+
# (Very rough estimate - real implementation needs odometry)
|
|
347
|
+
travel_time = distance / speed
|
|
348
|
+
time.sleep(min(travel_time, 10.0)) # Cap at 10 seconds
|
|
349
|
+
|
|
350
|
+
self.stop()
|
|
351
|
+
|
|
352
|
+
return ActionResult.ok(f"Walked to target")
|
|
353
|
+
|
|
354
|
+
def turn(self, angle: float, speed: float = 0.5) -> ActionResult:
|
|
355
|
+
"""Turn by angle (blocking)."""
|
|
356
|
+
if not self._check_connection():
|
|
357
|
+
return ActionResult.error("Not connected")
|
|
358
|
+
|
|
359
|
+
# Convert radians to degrees
|
|
360
|
+
angle_deg = math.degrees(angle)
|
|
361
|
+
|
|
362
|
+
# MechDog turn is via move(0, angle)
|
|
363
|
+
# Positive angle = turn left
|
|
364
|
+
turn_deg = max(-MAX_TURN_DEG, min(MAX_TURN_DEG, angle_deg))
|
|
365
|
+
|
|
366
|
+
# Estimate time based on turn rate
|
|
367
|
+
turn_time = abs(angle_deg) / 30.0 # Rough estimate
|
|
368
|
+
|
|
369
|
+
self._send_command(f"dog.move(0, {int(turn_deg)})")
|
|
370
|
+
self._is_moving = True
|
|
371
|
+
|
|
372
|
+
time.sleep(turn_time)
|
|
373
|
+
|
|
374
|
+
self.stop()
|
|
375
|
+
|
|
376
|
+
# Update pose estimate
|
|
377
|
+
roll, pitch, yaw = self._pose.orientation.to_euler()
|
|
378
|
+
yaw += angle
|
|
379
|
+
self._pose = Pose(
|
|
380
|
+
self._pose.position,
|
|
381
|
+
Quaternion.from_euler(roll, pitch, yaw)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return ActionResult.ok(f"Turned {angle_deg:.1f} degrees")
|
|
385
|
+
|
|
386
|
+
def turn_continuous(self, angular_velocity: float) -> ActionResult:
|
|
387
|
+
"""Turn continuously."""
|
|
388
|
+
if not self._check_connection():
|
|
389
|
+
return ActionResult.error("Not connected")
|
|
390
|
+
|
|
391
|
+
# Map angular velocity to turn angle
|
|
392
|
+
angle = int(angular_velocity * 20) # Rough mapping
|
|
393
|
+
angle = max(-MAX_TURN_DEG, min(MAX_TURN_DEG, angle))
|
|
394
|
+
|
|
395
|
+
self._send_command(f"dog.move(0, {angle})")
|
|
396
|
+
self._is_moving = True
|
|
397
|
+
|
|
398
|
+
self._velocity = Twist(
|
|
399
|
+
linear=Vector3.zero(),
|
|
400
|
+
angular=Vector3(0, 0, angular_velocity)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return ActionResult.ok(f"Turning at {angular_velocity:.2f} rad/s")
|
|
404
|
+
|
|
405
|
+
def stand(self) -> ActionResult:
|
|
406
|
+
"""Stand up."""
|
|
407
|
+
if not self._check_connection():
|
|
408
|
+
return ActionResult.error("Not connected")
|
|
409
|
+
|
|
410
|
+
self._send_command("dog.set_default_pose()")
|
|
411
|
+
self._is_moving = False
|
|
412
|
+
self._current_height = HEIGHT_DEFAULT
|
|
413
|
+
|
|
414
|
+
return ActionResult.ok("Standing")
|
|
415
|
+
|
|
416
|
+
def sit(self) -> ActionResult:
|
|
417
|
+
"""Sit down."""
|
|
418
|
+
if not self._check_connection():
|
|
419
|
+
return ActionResult.error("Not connected")
|
|
420
|
+
|
|
421
|
+
# Lower body to minimum height
|
|
422
|
+
return self.set_body_height(HEIGHT_MIN)
|
|
423
|
+
|
|
424
|
+
def lie_down(self) -> ActionResult:
|
|
425
|
+
"""Lie down (same as sit for MechDog)."""
|
|
426
|
+
return self.sit()
|
|
427
|
+
|
|
428
|
+
def set_gait(self, gait: GaitType) -> ActionResult:
|
|
429
|
+
"""Set gait (MechDog has limited gait support)."""
|
|
430
|
+
self._current_gait = gait
|
|
431
|
+
return ActionResult.ok(f"Gait set to {gait.name}")
|
|
432
|
+
|
|
433
|
+
def get_gait(self) -> GaitType:
|
|
434
|
+
"""Get current gait."""
|
|
435
|
+
return self._current_gait
|
|
436
|
+
|
|
437
|
+
def get_foot_positions(self) -> List[Vector3]:
|
|
438
|
+
"""Get foot positions (estimated from body height)."""
|
|
439
|
+
# Simplified: assume feet are at corners of body rectangle
|
|
440
|
+
h = self._current_height
|
|
441
|
+
return [
|
|
442
|
+
Vector3(BODY_LENGTH/2, BODY_WIDTH/2, -h), # Front left
|
|
443
|
+
Vector3(BODY_LENGTH/2, -BODY_WIDTH/2, -h), # Front right
|
|
444
|
+
Vector3(-BODY_LENGTH/2, BODY_WIDTH/2, -h), # Back left
|
|
445
|
+
Vector3(-BODY_LENGTH/2, -BODY_WIDTH/2, -h), # Back right
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
def get_joint_state(self) -> JointState:
|
|
449
|
+
"""Get all joint positions."""
|
|
450
|
+
if not self._check_connection():
|
|
451
|
+
return JointState()
|
|
452
|
+
|
|
453
|
+
result = self._send_command("print(dog.read_all_servo())")
|
|
454
|
+
|
|
455
|
+
# Parse result like "[2096, 1621, 2170, ...]"
|
|
456
|
+
try:
|
|
457
|
+
# Extract list from response
|
|
458
|
+
import re
|
|
459
|
+
match = re.search(r'\[([^\]]+)\]', result)
|
|
460
|
+
if match:
|
|
461
|
+
values = [int(x.strip()) for x in match.group(1).split(',')]
|
|
462
|
+
# Convert to radians (rough conversion)
|
|
463
|
+
positions = tuple(
|
|
464
|
+
(v - SERVO_CENTER) / SERVO_MAX * math.pi
|
|
465
|
+
for v in values
|
|
466
|
+
)
|
|
467
|
+
return JointState(positions=positions)
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
return JointState()
|
|
472
|
+
|
|
473
|
+
# =========================================================================
|
|
474
|
+
# BodyPoseInterface implementation
|
|
475
|
+
# =========================================================================
|
|
476
|
+
|
|
477
|
+
def set_body_height(self, height: float) -> ActionResult:
|
|
478
|
+
"""Set body height."""
|
|
479
|
+
if not self._check_connection():
|
|
480
|
+
return ActionResult.error("Not connected")
|
|
481
|
+
|
|
482
|
+
# Clamp to limits
|
|
483
|
+
height = max(HEIGHT_MIN, min(HEIGHT_MAX, height))
|
|
484
|
+
|
|
485
|
+
# MechDog uses set_pose() for body position
|
|
486
|
+
# Height is controlled via leg extension
|
|
487
|
+
# This is a simplified implementation
|
|
488
|
+
|
|
489
|
+
self._current_height = height
|
|
490
|
+
return ActionResult.ok(f"Height set to {height:.3f}m")
|
|
491
|
+
|
|
492
|
+
def get_body_height(self) -> float:
|
|
493
|
+
"""Get body height."""
|
|
494
|
+
return self._current_height
|
|
495
|
+
|
|
496
|
+
def get_height_limits(self) -> Tuple[float, float]:
|
|
497
|
+
"""Get height limits."""
|
|
498
|
+
return (HEIGHT_MIN, HEIGHT_MAX)
|
|
499
|
+
|
|
500
|
+
def set_body_orientation(
|
|
501
|
+
self,
|
|
502
|
+
roll: float = 0.0,
|
|
503
|
+
pitch: float = 0.0,
|
|
504
|
+
yaw: float = 0.0
|
|
505
|
+
) -> ActionResult:
|
|
506
|
+
"""Set body orientation."""
|
|
507
|
+
if not self._check_connection():
|
|
508
|
+
return ActionResult.error("Not connected")
|
|
509
|
+
|
|
510
|
+
# MechDog can tilt body via set_pose or transform
|
|
511
|
+
# Convert to degrees for MechDog
|
|
512
|
+
roll_deg = math.degrees(roll)
|
|
513
|
+
pitch_deg = math.degrees(pitch)
|
|
514
|
+
yaw_deg = math.degrees(yaw)
|
|
515
|
+
|
|
516
|
+
# MechDog's transform() takes (x, y, z, roll, pitch, yaw)
|
|
517
|
+
self._send_command(f"dog.transform(0, 0, 0, {roll_deg}, {pitch_deg}, {yaw_deg})")
|
|
518
|
+
|
|
519
|
+
self._current_orientation = (roll, pitch, yaw)
|
|
520
|
+
return ActionResult.ok(f"Orientation set to R={roll_deg:.1f} P={pitch_deg:.1f} Y={yaw_deg:.1f}")
|
|
521
|
+
|
|
522
|
+
def get_body_orientation(self) -> Tuple[float, float, float]:
|
|
523
|
+
"""Get body orientation."""
|
|
524
|
+
return self._current_orientation
|
|
525
|
+
|
|
526
|
+
def set_body_pose(
|
|
527
|
+
self,
|
|
528
|
+
height: Optional[float] = None,
|
|
529
|
+
roll: Optional[float] = None,
|
|
530
|
+
pitch: Optional[float] = None,
|
|
531
|
+
yaw: Optional[float] = None,
|
|
532
|
+
x_offset: Optional[float] = None,
|
|
533
|
+
y_offset: Optional[float] = None
|
|
534
|
+
) -> ActionResult:
|
|
535
|
+
"""Set combined body pose."""
|
|
536
|
+
if height is not None:
|
|
537
|
+
self.set_body_height(height)
|
|
538
|
+
|
|
539
|
+
# Get current values for any not specified
|
|
540
|
+
curr_r, curr_p, curr_y = self._current_orientation
|
|
541
|
+
if roll is None:
|
|
542
|
+
roll = curr_r
|
|
543
|
+
if pitch is None:
|
|
544
|
+
pitch = curr_p
|
|
545
|
+
if yaw is None:
|
|
546
|
+
yaw = curr_y
|
|
547
|
+
|
|
548
|
+
return self.set_body_orientation(roll, pitch, yaw)
|
|
549
|
+
|
|
550
|
+
# =========================================================================
|
|
551
|
+
# Internal methods
|
|
552
|
+
# =========================================================================
|
|
553
|
+
|
|
554
|
+
def _check_connection(self) -> bool:
|
|
555
|
+
"""Check if connected and ready."""
|
|
556
|
+
return self._connected and self._serial is not None and not self._estopped
|
|
557
|
+
|
|
558
|
+
def _send_command(self, cmd: str, wait: float = 0.5) -> str:
|
|
559
|
+
"""Send command to MicroPython REPL and get response."""
|
|
560
|
+
if not self._serial:
|
|
561
|
+
return ""
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
self._serial.reset_input_buffer()
|
|
565
|
+
self._serial.write(f"{cmd}\r\n".encode())
|
|
566
|
+
time.sleep(wait)
|
|
567
|
+
response = self._serial.read(4000).decode('utf-8', errors='ignore')
|
|
568
|
+
return response
|
|
569
|
+
except Exception as e:
|
|
570
|
+
return f"Error: {e}"
|
|
571
|
+
|
|
572
|
+
# =========================================================================
|
|
573
|
+
# MechDog-specific methods (not in interface)
|
|
574
|
+
# =========================================================================
|
|
575
|
+
|
|
576
|
+
def read_servo(self, servo_id: int) -> Optional[int]:
|
|
577
|
+
"""Read single servo position (raw value)."""
|
|
578
|
+
if not self._check_connection():
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
result = self._send_command(f"print(dog.read_servo({servo_id}))")
|
|
582
|
+
try:
|
|
583
|
+
# Parse integer from response
|
|
584
|
+
import re
|
|
585
|
+
match = re.search(r'(\d+)', result.split('\n')[-2])
|
|
586
|
+
if match:
|
|
587
|
+
return int(match.group(1))
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
return None
|
|
591
|
+
|
|
592
|
+
def set_servo(self, servo_id: int, position: int, time_ms: int = 500) -> ActionResult:
|
|
593
|
+
"""Set single servo position (raw value)."""
|
|
594
|
+
if not self._check_connection():
|
|
595
|
+
return ActionResult.error("Not connected")
|
|
596
|
+
|
|
597
|
+
self._send_command(f"dog.set_servo({servo_id}, {position}, {time_ms})")
|
|
598
|
+
return ActionResult.ok(f"Servo {servo_id} set to {position}")
|
|
599
|
+
|
|
600
|
+
def run_action(self, action_name: str) -> ActionResult:
|
|
601
|
+
"""Run a pre-programmed action."""
|
|
602
|
+
if not self._check_connection():
|
|
603
|
+
return ActionResult.error("Not connected")
|
|
604
|
+
|
|
605
|
+
self._send_command(f"dog.action_run('{action_name}')")
|
|
606
|
+
return ActionResult.ok(f"Running action: {action_name}")
|