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.
Files changed (116) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +399 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  116. {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
- class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, RobotInterface):
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
- return self._connected and self._serial is not None and self._serial.is_open
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._serial:
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 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)
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 stride from speed (simple mapping)
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
- return self._connected and self._serial is not None and not self._estopped
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
- if not self._serial:
812
+ transport = self._transport
813
+ if not transport:
561
814
  return ""
562
815
 
563
816
  try:
564
- self._serial.reset_input_buffer()
565
- self._serial.write(f"{cmd}\r\n".encode())
817
+ transport.reset_input_buffer()
818
+ transport.write(f"{cmd}\r\n".encode())
566
819
  time.sleep(wait)
567
- response = self._serial.read(4000).decode('utf-8', errors='ignore')
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, action_name: str) -> ActionResult:
601
- """Run a pre-programmed action."""
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(f"dog.action_run('{action_name}')")
606
- return ActionResult.ok(f"Running action: {action_name}")
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")