foodforthought-cli 0.2.8__py3-none-any.whl → 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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +12 -0
- ate/behaviors/approach.py +399 -0
- ate/cli.py +855 -4551
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +18 -6
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +360 -24
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +16 -0
- ate/interfaces/base.py +2 -0
- ate/interfaces/sensors.py +247 -0
- ate/llm_proxy.py +239 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +42 -3
- ate/recording/session.py +12 -2
- ate/recording/visual.py +416 -0
- ate/robot/__init__.py +142 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +88 -3
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +143 -11
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +104 -2
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +6 -0
- ate/robot/registry.py +5 -2
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +285 -3
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +9 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/drivers/mechdog.py
CHANGED
|
@@ -36,6 +36,7 @@ from ..interfaces.types import (
|
|
|
36
36
|
RobotMode,
|
|
37
37
|
BatteryState,
|
|
38
38
|
IMUReading,
|
|
39
|
+
Image,
|
|
39
40
|
)
|
|
40
41
|
from ..interfaces.base import (
|
|
41
42
|
RobotInterface,
|
|
@@ -45,6 +46,9 @@ from ..interfaces.base import (
|
|
|
45
46
|
)
|
|
46
47
|
from ..interfaces.locomotion import QuadrupedLocomotion
|
|
47
48
|
from ..interfaces.body import BodyPoseInterface
|
|
49
|
+
from ..interfaces.perception import CameraInterface
|
|
50
|
+
from ..interfaces.sensors import DistanceSensorInterface, DistanceReading, DistanceSensorType
|
|
51
|
+
from .wifi_camera import WiFiCamera, WiFiCameraConfig
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
# =============================================================================
|
|
@@ -70,6 +74,30 @@ HEIGHT_DEFAULT = 0.12
|
|
|
70
74
|
MAX_STRIDE_MM = 100 # Maximum stride in mm
|
|
71
75
|
MAX_TURN_DEG = 30 # Maximum turn rate in degrees
|
|
72
76
|
|
|
77
|
+
# Arm servo IDs (for models with arm attachment)
|
|
78
|
+
# CORRECTED 2026-01-03: Discovered via vision-based servo identification
|
|
79
|
+
# Vision diff analysis showed:
|
|
80
|
+
# - Servos 1,2,4,5,6,7 are LEG servos (large movement, body tilt)
|
|
81
|
+
# - Servos 8,9 are ARM servos (medium movement, arm only)
|
|
82
|
+
# - Servo 11 is GRIPPER (small, localized movement at tip)
|
|
83
|
+
SERVO_GRIPPER = 11 # Gripper servo (open/close)
|
|
84
|
+
SERVO_SHOULDER = 8 # Shoulder/arm raise-lower
|
|
85
|
+
SERVO_ELBOW = 9 # Elbow extend/retract
|
|
86
|
+
|
|
87
|
+
# Calibrated arm servo positions (PWM microseconds: 500-2500)
|
|
88
|
+
# Discovered empirically 2026-01-03 via systematic testing
|
|
89
|
+
GRIPPER_OPEN = 500 # Gripper jaws spread wide
|
|
90
|
+
GRIPPER_CLOSED = 2400 # Gripper jaws clamped
|
|
91
|
+
GRIPPER_NEUTRAL = 1500 # Gripper partially open
|
|
92
|
+
|
|
93
|
+
SHOULDER_UP = 2200 # Arm raised up (servo 4 high)
|
|
94
|
+
SHOULDER_DOWN = 800 # Arm lowered for pickup (servo 4 low)
|
|
95
|
+
SHOULDER_NEUTRAL = 1500 # Arm at middle height
|
|
96
|
+
|
|
97
|
+
ELBOW_RETRACTED = 2000 # Arm pulled in toward body (servo 5 high)
|
|
98
|
+
ELBOW_EXTENDED = 800 # Arm reaching forward (servo 5 low)
|
|
99
|
+
ELBOW_NEUTRAL = 1500 # Arm at middle extension
|
|
100
|
+
|
|
73
101
|
|
|
74
102
|
@dataclass
|
|
75
103
|
class MechDogConfig:
|
|
@@ -78,10 +106,21 @@ class MechDogConfig:
|
|
|
78
106
|
baud_rate: int = 115200
|
|
79
107
|
timeout: float = 2.0
|
|
80
108
|
has_arm: bool = False
|
|
109
|
+
# Camera configuration
|
|
81
110
|
has_camera: bool = False
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
111
|
+
camera_ip: str = "" # IP of visual module (e.g., "192.168.1.100")
|
|
112
|
+
camera_port: int = 80 # HTTP port
|
|
113
|
+
camera_stream_port: int = 81 # MJPEG stream port
|
|
114
|
+
# Ultrasonic sensor (HC-SR04 style on ESP32)
|
|
115
|
+
has_ultrasonic: bool = True # MechDog has built-in ultrasonic
|
|
116
|
+
# BLE configuration (alternative to USB serial)
|
|
117
|
+
use_ble: bool = False # Use BLE instead of serial
|
|
118
|
+
ble_address: str = "" # BLE device address (MAC on Windows/Linux, UUID on macOS)
|
|
119
|
+
ble_service_uuid: str = "0000ffe0-0000-1000-8000-00805f9b34fb" # HM-10/ESP32 BLE serial
|
|
120
|
+
ble_char_uuid: str = "0000ffe1-0000-1000-8000-00805f9b34fb" # RX/TX characteristic
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, CameraInterface, DistanceSensorInterface, RobotInterface):
|
|
85
124
|
"""
|
|
86
125
|
Driver for HiWonder MechDog quadruped robot.
|
|
87
126
|
|
|
@@ -89,6 +128,8 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
89
128
|
- QuadrupedLocomotion: walk, turn, stand, sit, etc.
|
|
90
129
|
- BodyPoseInterface: height, roll, pitch, yaw control
|
|
91
130
|
- SafetyInterface: emergency stop, battery monitoring
|
|
131
|
+
- CameraInterface: image capture from visual module (optional)
|
|
132
|
+
- DistanceSensorInterface: ultrasonic distance sensing
|
|
92
133
|
- RobotInterface: connection lifecycle, status
|
|
93
134
|
|
|
94
135
|
Example:
|
|
@@ -99,6 +140,9 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
99
140
|
dog.walk(Vector3.forward(), speed=0.3)
|
|
100
141
|
dog.set_body_height(0.10)
|
|
101
142
|
|
|
143
|
+
# If camera is configured:
|
|
144
|
+
image = dog.get_image()
|
|
145
|
+
|
|
102
146
|
dog.disconnect()
|
|
103
147
|
"""
|
|
104
148
|
|
|
@@ -107,11 +151,12 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
107
151
|
Initialize MechDog driver.
|
|
108
152
|
|
|
109
153
|
Args:
|
|
110
|
-
port: Serial port for MechDog connection
|
|
154
|
+
port: Serial port or BLE address for MechDog connection
|
|
111
155
|
config: Optional configuration
|
|
112
156
|
"""
|
|
113
157
|
self.config = config or MechDogConfig(port=port)
|
|
114
158
|
self._serial: Optional[serial.Serial] = None
|
|
159
|
+
self._ble_transport = None # BLE transport when use_ble=True
|
|
115
160
|
self._connected = False
|
|
116
161
|
self._estopped = False
|
|
117
162
|
self._current_gait = GaitType.WALK
|
|
@@ -125,6 +170,23 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
125
170
|
self._pose = Pose.identity()
|
|
126
171
|
self._velocity = Twist.zero()
|
|
127
172
|
|
|
173
|
+
# Camera (visual module)
|
|
174
|
+
self._camera: Optional[WiFiCamera] = None
|
|
175
|
+
if self.config.has_camera and self.config.camera_ip:
|
|
176
|
+
camera_config = WiFiCameraConfig(
|
|
177
|
+
ip=self.config.camera_ip,
|
|
178
|
+
port=self.config.camera_port,
|
|
179
|
+
stream_port=self.config.camera_stream_port,
|
|
180
|
+
)
|
|
181
|
+
self._camera = WiFiCamera(camera_config)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def _transport(self):
|
|
185
|
+
"""Get the active transport (serial or BLE)."""
|
|
186
|
+
if self.config.use_ble and self._ble_transport:
|
|
187
|
+
return self._ble_transport
|
|
188
|
+
return self._serial
|
|
189
|
+
|
|
128
190
|
# =========================================================================
|
|
129
191
|
# RobotInterface implementation
|
|
130
192
|
# =========================================================================
|
|
@@ -144,6 +206,9 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
144
206
|
if self.config.has_camera:
|
|
145
207
|
capabilities.add(Capability.CAMERA)
|
|
146
208
|
|
|
209
|
+
if self.config.has_ultrasonic:
|
|
210
|
+
capabilities.add(Capability.DISTANCE_SENSOR)
|
|
211
|
+
|
|
147
212
|
return RobotInfo(
|
|
148
213
|
name="MechDog",
|
|
149
214
|
model="hiwonder_mechdog",
|
|
@@ -159,7 +224,58 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
159
224
|
)
|
|
160
225
|
|
|
161
226
|
def connect(self) -> ActionResult:
|
|
162
|
-
"""Connect to MechDog via serial."""
|
|
227
|
+
"""Connect to MechDog via serial or BLE."""
|
|
228
|
+
if self.config.use_ble:
|
|
229
|
+
return self._connect_ble()
|
|
230
|
+
return self._connect_serial()
|
|
231
|
+
|
|
232
|
+
def _connect_ble(self) -> ActionResult:
|
|
233
|
+
"""Connect to MechDog via Bluetooth Low Energy."""
|
|
234
|
+
try:
|
|
235
|
+
from .ble_transport import BLETransport
|
|
236
|
+
except ImportError:
|
|
237
|
+
return ActionResult.error("BLE transport requires bleak. Run: pip install bleak")
|
|
238
|
+
|
|
239
|
+
if not self.config.ble_address:
|
|
240
|
+
return ActionResult.error("BLE address not configured. Set config.ble_address")
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
self._ble_transport = BLETransport(
|
|
244
|
+
address=self.config.ble_address,
|
|
245
|
+
service_uuid=self.config.ble_service_uuid,
|
|
246
|
+
char_uuid=self.config.ble_char_uuid,
|
|
247
|
+
timeout=self.config.timeout,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if not self._ble_transport.connect():
|
|
251
|
+
return ActionResult.error("Failed to connect to BLE device")
|
|
252
|
+
|
|
253
|
+
time.sleep(0.5)
|
|
254
|
+
|
|
255
|
+
# Enter friendly REPL mode
|
|
256
|
+
self._ble_transport.write(b'\x03') # Ctrl+C to interrupt
|
|
257
|
+
time.sleep(0.3)
|
|
258
|
+
self._ble_transport.write(b'\x02') # Ctrl+B for friendly REPL
|
|
259
|
+
time.sleep(0.5)
|
|
260
|
+
self._ble_transport.reset_input_buffer()
|
|
261
|
+
|
|
262
|
+
# Initialize MechDog library
|
|
263
|
+
result = self._send_command("from HW_MechDog import MechDog")
|
|
264
|
+
if "Error" in result or "Traceback" in result:
|
|
265
|
+
return ActionResult.error(f"Failed to import HW_MechDog: {result}")
|
|
266
|
+
|
|
267
|
+
result = self._send_command("dog = MechDog()")
|
|
268
|
+
if "Error" in result or "Traceback" in result:
|
|
269
|
+
return ActionResult.error(f"Failed to create MechDog: {result}")
|
|
270
|
+
|
|
271
|
+
self._connected = True
|
|
272
|
+
return ActionResult.ok(f"Connected to MechDog via BLE ({self.config.ble_address})")
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return ActionResult.error(f"BLE connection failed: {e}")
|
|
276
|
+
|
|
277
|
+
def _connect_serial(self) -> ActionResult:
|
|
278
|
+
"""Connect to MechDog via USB serial."""
|
|
163
279
|
if not HAS_SERIAL:
|
|
164
280
|
return ActionResult.error("pyserial not installed. Run: pip install pyserial")
|
|
165
281
|
|
|
@@ -195,22 +311,42 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
195
311
|
|
|
196
312
|
def disconnect(self) -> ActionResult:
|
|
197
313
|
"""Disconnect from MechDog."""
|
|
314
|
+
try:
|
|
315
|
+
# Stop any movement first
|
|
316
|
+
self.stop()
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
# Close BLE transport if active
|
|
321
|
+
if self._ble_transport:
|
|
322
|
+
try:
|
|
323
|
+
self._ble_transport.close()
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
finally:
|
|
327
|
+
self._ble_transport = None
|
|
328
|
+
|
|
329
|
+
# Close serial if active
|
|
198
330
|
if self._serial:
|
|
199
331
|
try:
|
|
200
|
-
# Stop any movement first
|
|
201
|
-
self.stop()
|
|
202
332
|
self._serial.close()
|
|
203
333
|
except Exception:
|
|
204
334
|
pass
|
|
205
335
|
finally:
|
|
206
336
|
self._serial = None
|
|
207
|
-
self._connected = False
|
|
208
337
|
|
|
338
|
+
self._connected = False
|
|
209
339
|
return ActionResult.ok("Disconnected")
|
|
210
340
|
|
|
211
341
|
def is_connected(self) -> bool:
|
|
212
342
|
"""Check connection status."""
|
|
213
|
-
|
|
343
|
+
if not self._connected:
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
if self.config.use_ble:
|
|
347
|
+
return self._ble_transport is not None and self._ble_transport.is_open
|
|
348
|
+
|
|
349
|
+
return self._serial is not None and self._serial.is_open
|
|
214
350
|
|
|
215
351
|
def get_status(self) -> RobotStatus:
|
|
216
352
|
"""Get robot status."""
|
|
@@ -239,7 +375,7 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
239
375
|
self._estopped = True
|
|
240
376
|
self._is_moving = False
|
|
241
377
|
|
|
242
|
-
if self.
|
|
378
|
+
if self._transport:
|
|
243
379
|
# Send stop command
|
|
244
380
|
self._send_command("dog.move(0, 0)")
|
|
245
381
|
|
|
@@ -259,6 +395,119 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
259
395
|
# MechDog doesn't report battery state via REPL
|
|
260
396
|
return None
|
|
261
397
|
|
|
398
|
+
# =========================================================================
|
|
399
|
+
# CameraInterface implementation
|
|
400
|
+
# =========================================================================
|
|
401
|
+
|
|
402
|
+
def get_image(self) -> Image:
|
|
403
|
+
"""
|
|
404
|
+
Capture image from visual module.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Image with RGB data, or empty Image if camera not configured
|
|
408
|
+
"""
|
|
409
|
+
if not self._camera:
|
|
410
|
+
return Image(
|
|
411
|
+
data=b"",
|
|
412
|
+
width=0,
|
|
413
|
+
height=0,
|
|
414
|
+
format="error",
|
|
415
|
+
encoding="none",
|
|
416
|
+
timestamp=time.time(),
|
|
417
|
+
metadata={"error": "Camera not configured"},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return self._camera.get_image()
|
|
421
|
+
|
|
422
|
+
def get_resolution(self) -> Tuple[int, int]:
|
|
423
|
+
"""Get camera resolution."""
|
|
424
|
+
if not self._camera:
|
|
425
|
+
return (0, 0)
|
|
426
|
+
return self._camera.get_resolution()
|
|
427
|
+
|
|
428
|
+
def get_intrinsics(self) -> dict:
|
|
429
|
+
"""Get camera intrinsic parameters."""
|
|
430
|
+
if not self._camera:
|
|
431
|
+
return {}
|
|
432
|
+
return self._camera.get_intrinsics()
|
|
433
|
+
|
|
434
|
+
def get_frame_id(self) -> str:
|
|
435
|
+
"""Get coordinate frame ID for camera."""
|
|
436
|
+
return "mechdog_camera_link"
|
|
437
|
+
|
|
438
|
+
def has_camera(self) -> bool:
|
|
439
|
+
"""Check if camera is available."""
|
|
440
|
+
return self._camera is not None
|
|
441
|
+
|
|
442
|
+
def is_camera_connected(self) -> bool:
|
|
443
|
+
"""Check if camera is reachable."""
|
|
444
|
+
if not self._camera:
|
|
445
|
+
return False
|
|
446
|
+
return self._camera.is_connected()
|
|
447
|
+
|
|
448
|
+
# =========================================================================
|
|
449
|
+
# DistanceSensorInterface implementation
|
|
450
|
+
# =========================================================================
|
|
451
|
+
|
|
452
|
+
def get_distance(self) -> DistanceReading:
|
|
453
|
+
"""
|
|
454
|
+
Read distance from ultrasonic sensor.
|
|
455
|
+
|
|
456
|
+
MechDog's ultrasonic sensor is mounted on the front.
|
|
457
|
+
Returns distance in meters.
|
|
458
|
+
"""
|
|
459
|
+
if not self._check_connection():
|
|
460
|
+
return DistanceReading.invalid()
|
|
461
|
+
|
|
462
|
+
if not self.config.has_ultrasonic:
|
|
463
|
+
return DistanceReading.invalid()
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
# MechDog ultrasonic reading via HW_MechDog library
|
|
467
|
+
# The sensor returns distance in centimeters
|
|
468
|
+
result = self._send_command("print(dog.read_ultrasonic())")
|
|
469
|
+
|
|
470
|
+
# Parse result - typically returns just a number
|
|
471
|
+
import re
|
|
472
|
+
match = re.search(r'(\d+\.?\d*)', result.split('\n')[-2] if '\n' in result else result)
|
|
473
|
+
if match:
|
|
474
|
+
distance_cm = float(match.group(1))
|
|
475
|
+
distance_m = distance_cm / 100.0
|
|
476
|
+
|
|
477
|
+
# Validate reading is in range
|
|
478
|
+
if distance_m < self.get_min_range() or distance_m > self.get_max_range():
|
|
479
|
+
return DistanceReading(
|
|
480
|
+
distance=distance_m,
|
|
481
|
+
valid=False,
|
|
482
|
+
sensor_type=DistanceSensorType.ULTRASONIC,
|
|
483
|
+
timestamp=time.time(),
|
|
484
|
+
confidence=0.5,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
return DistanceReading(
|
|
488
|
+
distance=distance_m,
|
|
489
|
+
valid=True,
|
|
490
|
+
sensor_type=DistanceSensorType.ULTRASONIC,
|
|
491
|
+
timestamp=time.time(),
|
|
492
|
+
confidence=0.9,
|
|
493
|
+
)
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
return DistanceReading.invalid()
|
|
498
|
+
|
|
499
|
+
def get_min_range(self) -> float:
|
|
500
|
+
"""Get minimum measurable range (2cm for HC-SR04)."""
|
|
501
|
+
return 0.02 # 2cm
|
|
502
|
+
|
|
503
|
+
def get_max_range(self) -> float:
|
|
504
|
+
"""Get maximum measurable range (4m for HC-SR04)."""
|
|
505
|
+
return 4.0 # 4m
|
|
506
|
+
|
|
507
|
+
def get_sensor_type(self) -> DistanceSensorType:
|
|
508
|
+
"""Get the type of distance sensor."""
|
|
509
|
+
return DistanceSensorType.ULTRASONIC
|
|
510
|
+
|
|
262
511
|
# =========================================================================
|
|
263
512
|
# QuadrupedLocomotion implementation
|
|
264
513
|
# =========================================================================
|
|
@@ -294,12 +543,13 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
294
543
|
if self._estopped:
|
|
295
544
|
return ActionResult.error("Emergency stop active")
|
|
296
545
|
|
|
297
|
-
# Convert direction and speed to MechDog
|
|
298
|
-
# MechDog uses: move(
|
|
299
|
-
#
|
|
300
|
-
#
|
|
546
|
+
# Convert direction and speed to MechDog velocity control
|
|
547
|
+
# MechDog uses: move(forward_vel, turn_vel)
|
|
548
|
+
# forward_vel: -100 to +100 (negative = backward)
|
|
549
|
+
# turn_vel: -100 to +100 (negative = turn left, positive = turn right)
|
|
550
|
+
# Note: This is velocity-based, NOT distance-based!
|
|
301
551
|
|
|
302
|
-
# Calculate
|
|
552
|
+
# Calculate forward velocity from speed (simple mapping)
|
|
303
553
|
stride = int(min(MAX_STRIDE_MM, max(-MAX_STRIDE_MM, speed * 200)))
|
|
304
554
|
|
|
305
555
|
# Apply direction
|
|
@@ -553,18 +803,21 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
553
803
|
|
|
554
804
|
def _check_connection(self) -> bool:
|
|
555
805
|
"""Check if connected and ready."""
|
|
556
|
-
|
|
806
|
+
if self._estopped:
|
|
807
|
+
return False
|
|
808
|
+
return self._connected and self._transport is not None
|
|
557
809
|
|
|
558
810
|
def _send_command(self, cmd: str, wait: float = 0.5) -> str:
|
|
559
811
|
"""Send command to MicroPython REPL and get response."""
|
|
560
|
-
|
|
812
|
+
transport = self._transport
|
|
813
|
+
if not transport:
|
|
561
814
|
return ""
|
|
562
815
|
|
|
563
816
|
try:
|
|
564
|
-
|
|
565
|
-
|
|
817
|
+
transport.reset_input_buffer()
|
|
818
|
+
transport.write(f"{cmd}\r\n".encode())
|
|
566
819
|
time.sleep(wait)
|
|
567
|
-
response =
|
|
820
|
+
response = transport.read(4000).decode('utf-8', errors='ignore')
|
|
568
821
|
return response
|
|
569
822
|
except Exception as e:
|
|
570
823
|
return f"Error: {e}"
|
|
@@ -597,10 +850,93 @@ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, Rob
|
|
|
597
850
|
self._send_command(f"dog.set_servo({servo_id}, {position}, {time_ms})")
|
|
598
851
|
return ActionResult.ok(f"Servo {servo_id} set to {position}")
|
|
599
852
|
|
|
600
|
-
def run_action(self,
|
|
601
|
-
"""
|
|
853
|
+
def run_action(self, action_id: int) -> ActionResult:
|
|
854
|
+
"""
|
|
855
|
+
Run a pre-programmed action by ID.
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
action_id: Integer ID of the action to run
|
|
859
|
+
|
|
860
|
+
Note: Action IDs must be discovered experimentally as
|
|
861
|
+
the MechDog firmware doesn't document available actions.
|
|
862
|
+
Common IDs found: 0-15 (varies by firmware version)
|
|
863
|
+
"""
|
|
864
|
+
if not self._check_connection():
|
|
865
|
+
return ActionResult.error("Not connected")
|
|
866
|
+
|
|
867
|
+
self._send_command(f"dog.action_run({action_id})")
|
|
868
|
+
return ActionResult.ok(f"Running action ID: {action_id}")
|
|
869
|
+
|
|
870
|
+
def wait_action(self) -> ActionResult:
|
|
871
|
+
"""Wait for current action to complete."""
|
|
602
872
|
if not self._check_connection():
|
|
603
873
|
return ActionResult.error("Not connected")
|
|
604
874
|
|
|
605
|
-
self._send_command(
|
|
606
|
-
return ActionResult.ok(
|
|
875
|
+
self._send_command("dog.wait_action_over()")
|
|
876
|
+
return ActionResult.ok("Action complete")
|
|
877
|
+
|
|
878
|
+
def stop_action(self) -> ActionResult:
|
|
879
|
+
"""Stop current action."""
|
|
880
|
+
if not self._check_connection():
|
|
881
|
+
return ActionResult.error("Not connected")
|
|
882
|
+
|
|
883
|
+
self._send_command("dog.stop_action()")
|
|
884
|
+
return ActionResult.ok("Action stopped")
|
|
885
|
+
|
|
886
|
+
# =========================================================================
|
|
887
|
+
# Arm control methods (for models with arm attachment)
|
|
888
|
+
# =========================================================================
|
|
889
|
+
|
|
890
|
+
def gripper_open(self, time_ms: int = 400) -> ActionResult:
|
|
891
|
+
"""Open gripper to grab position."""
|
|
892
|
+
return self.set_servo(SERVO_GRIPPER, GRIPPER_OPEN, time_ms)
|
|
893
|
+
|
|
894
|
+
def gripper_close(self, time_ms: int = 400) -> ActionResult:
|
|
895
|
+
"""Close gripper to grasp object."""
|
|
896
|
+
return self.set_servo(SERVO_GRIPPER, GRIPPER_CLOSED, time_ms)
|
|
897
|
+
|
|
898
|
+
def arm_up(self, time_ms: int = 600) -> ActionResult:
|
|
899
|
+
"""Raise arm to carry position."""
|
|
900
|
+
self.set_servo(SERVO_ELBOW, ELBOW_RETRACTED, time_ms)
|
|
901
|
+
return self.set_servo(SERVO_SHOULDER, SHOULDER_UP, time_ms)
|
|
902
|
+
|
|
903
|
+
def arm_down(self, time_ms: int = 600) -> ActionResult:
|
|
904
|
+
"""Lower arm to pickup position."""
|
|
905
|
+
self.set_servo(SERVO_SHOULDER, SHOULDER_DOWN, time_ms)
|
|
906
|
+
return self.set_servo(SERVO_ELBOW, ELBOW_EXTENDED, time_ms)
|
|
907
|
+
|
|
908
|
+
def arm_neutral(self, time_ms: int = 500) -> ActionResult:
|
|
909
|
+
"""Move arm to neutral position."""
|
|
910
|
+
self.set_servo(SERVO_GRIPPER, GRIPPER_NEUTRAL, time_ms)
|
|
911
|
+
self.set_servo(SERVO_SHOULDER, SHOULDER_NEUTRAL, time_ms)
|
|
912
|
+
return self.set_servo(SERVO_ELBOW, ELBOW_NEUTRAL, time_ms)
|
|
913
|
+
|
|
914
|
+
def pickup_sequence(self, grab_delay: float = 0.3) -> ActionResult:
|
|
915
|
+
"""
|
|
916
|
+
Execute pickup sequence: lower arm, close gripper, lift.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
grab_delay: Seconds to wait between lowering and closing gripper
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
ActionResult indicating success/failure
|
|
923
|
+
"""
|
|
924
|
+
if not self.config.has_arm:
|
|
925
|
+
return ActionResult.error("Robot does not have arm attachment")
|
|
926
|
+
|
|
927
|
+
# Open gripper
|
|
928
|
+
self.gripper_open()
|
|
929
|
+
time.sleep(0.3)
|
|
930
|
+
|
|
931
|
+
# Lower arm
|
|
932
|
+
self.arm_down()
|
|
933
|
+
time.sleep(grab_delay)
|
|
934
|
+
|
|
935
|
+
# Close gripper
|
|
936
|
+
self.gripper_close()
|
|
937
|
+
time.sleep(0.3)
|
|
938
|
+
|
|
939
|
+
# Lift arm
|
|
940
|
+
self.arm_up()
|
|
941
|
+
|
|
942
|
+
return ActionResult.ok("Pickup sequence complete")
|