foodforthought-cli 0.2.7__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 (131) 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 +100 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/behaviors/common.py +686 -0
  9. ate/behaviors/tree.py +454 -0
  10. ate/cli.py +855 -3995
  11. ate/client.py +90 -0
  12. ate/commands/__init__.py +168 -0
  13. ate/commands/auth.py +389 -0
  14. ate/commands/bridge.py +448 -0
  15. ate/commands/data.py +185 -0
  16. ate/commands/deps.py +111 -0
  17. ate/commands/generate.py +384 -0
  18. ate/commands/memory.py +907 -0
  19. ate/commands/parts.py +166 -0
  20. ate/commands/primitive.py +399 -0
  21. ate/commands/protocol.py +288 -0
  22. ate/commands/recording.py +524 -0
  23. ate/commands/repo.py +154 -0
  24. ate/commands/simulation.py +291 -0
  25. ate/commands/skill.py +303 -0
  26. ate/commands/skills.py +487 -0
  27. ate/commands/team.py +147 -0
  28. ate/commands/workflow.py +271 -0
  29. ate/detection/__init__.py +38 -0
  30. ate/detection/base.py +142 -0
  31. ate/detection/color_detector.py +399 -0
  32. ate/detection/trash_detector.py +322 -0
  33. ate/drivers/__init__.py +39 -0
  34. ate/drivers/ble_transport.py +405 -0
  35. ate/drivers/mechdog.py +942 -0
  36. ate/drivers/wifi_camera.py +477 -0
  37. ate/interfaces/__init__.py +187 -0
  38. ate/interfaces/base.py +273 -0
  39. ate/interfaces/body.py +267 -0
  40. ate/interfaces/detection.py +282 -0
  41. ate/interfaces/locomotion.py +422 -0
  42. ate/interfaces/manipulation.py +408 -0
  43. ate/interfaces/navigation.py +389 -0
  44. ate/interfaces/perception.py +362 -0
  45. ate/interfaces/sensors.py +247 -0
  46. ate/interfaces/types.py +371 -0
  47. ate/llm_proxy.py +239 -0
  48. ate/mcp_server.py +387 -0
  49. ate/memory/__init__.py +35 -0
  50. ate/memory/cloud.py +244 -0
  51. ate/memory/context.py +269 -0
  52. ate/memory/embeddings.py +184 -0
  53. ate/memory/export.py +26 -0
  54. ate/memory/merge.py +146 -0
  55. ate/memory/migrate/__init__.py +34 -0
  56. ate/memory/migrate/base.py +89 -0
  57. ate/memory/migrate/pipeline.py +189 -0
  58. ate/memory/migrate/sources/__init__.py +13 -0
  59. ate/memory/migrate/sources/chroma.py +170 -0
  60. ate/memory/migrate/sources/pinecone.py +120 -0
  61. ate/memory/migrate/sources/qdrant.py +110 -0
  62. ate/memory/migrate/sources/weaviate.py +160 -0
  63. ate/memory/reranker.py +353 -0
  64. ate/memory/search.py +26 -0
  65. ate/memory/store.py +548 -0
  66. ate/recording/__init__.py +83 -0
  67. ate/recording/demonstration.py +378 -0
  68. ate/recording/session.py +415 -0
  69. ate/recording/upload.py +304 -0
  70. ate/recording/visual.py +416 -0
  71. ate/recording/wrapper.py +95 -0
  72. ate/robot/__init__.py +221 -0
  73. ate/robot/agentic_servo.py +856 -0
  74. ate/robot/behaviors.py +493 -0
  75. ate/robot/ble_capture.py +1000 -0
  76. ate/robot/ble_enumerate.py +506 -0
  77. ate/robot/calibration.py +668 -0
  78. ate/robot/calibration_state.py +388 -0
  79. ate/robot/commands.py +3735 -0
  80. ate/robot/direction_calibration.py +554 -0
  81. ate/robot/discovery.py +441 -0
  82. ate/robot/introspection.py +330 -0
  83. ate/robot/llm_system_id.py +654 -0
  84. ate/robot/locomotion_calibration.py +508 -0
  85. ate/robot/manager.py +270 -0
  86. ate/robot/marker_generator.py +611 -0
  87. ate/robot/perception.py +502 -0
  88. ate/robot/primitives.py +614 -0
  89. ate/robot/profiles.py +281 -0
  90. ate/robot/registry.py +322 -0
  91. ate/robot/servo_mapper.py +1153 -0
  92. ate/robot/skill_upload.py +675 -0
  93. ate/robot/target_calibration.py +500 -0
  94. ate/robot/teach.py +515 -0
  95. ate/robot/types.py +242 -0
  96. ate/robot/visual_labeler.py +1048 -0
  97. ate/robot/visual_servo_loop.py +494 -0
  98. ate/robot/visual_servoing.py +570 -0
  99. ate/robot/visual_system_id.py +906 -0
  100. ate/transports/__init__.py +121 -0
  101. ate/transports/base.py +394 -0
  102. ate/transports/ble.py +405 -0
  103. ate/transports/hybrid.py +444 -0
  104. ate/transports/serial.py +345 -0
  105. ate/urdf/__init__.py +30 -0
  106. ate/urdf/capture.py +582 -0
  107. ate/urdf/cloud.py +491 -0
  108. ate/urdf/collision.py +271 -0
  109. ate/urdf/commands.py +708 -0
  110. ate/urdf/depth.py +360 -0
  111. ate/urdf/inertial.py +312 -0
  112. ate/urdf/kinematics.py +330 -0
  113. ate/urdf/lifting.py +415 -0
  114. ate/urdf/meshing.py +300 -0
  115. ate/urdf/models/__init__.py +110 -0
  116. ate/urdf/models/depth_anything.py +253 -0
  117. ate/urdf/models/sam2.py +324 -0
  118. ate/urdf/motion_analysis.py +396 -0
  119. ate/urdf/pipeline.py +468 -0
  120. ate/urdf/scale.py +256 -0
  121. ate/urdf/scan_session.py +411 -0
  122. ate/urdf/segmentation.py +299 -0
  123. ate/urdf/synthesis.py +319 -0
  124. ate/urdf/topology.py +336 -0
  125. ate/urdf/validation.py +371 -0
  126. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
  127. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  128. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  129. foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
  130. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  131. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,508 @@
1
+ """
2
+ Locomotion Characterization for Mobile Robots.
3
+
4
+ This module provides tools to characterize the relationship between
5
+ motor commands and actual robot displacement. This is the missing
6
+ piece that connects "move forward 100mm" to actual motor commands.
7
+
8
+ The key insight: vendor APIs like `dog.move(vel, turn)` are velocity-based
9
+ with unknown relationships to actual displacement. This module measures
10
+ those relationships and stores them for later use.
11
+
12
+ Usage:
13
+ from ate.robot.locomotion_calibration import LocomotionCalibrator
14
+
15
+ # Setup with ArUco marker on ground
16
+ calibrator = LocomotionCalibrator(driver, camera)
17
+
18
+ # Run characterization
19
+ results = calibrator.characterize_locomotion()
20
+
21
+ # Now we know: move(50, 0) for 1 sec = 35mm forward
22
+
23
+ # Use for planning
24
+ time_needed = calibrator.time_for_distance(100) # mm
25
+ driver.move(50, 0)
26
+ time.sleep(time_needed)
27
+ driver.stop()
28
+
29
+ Pain Points This Solves:
30
+ 1. "move(50, 0) for how long to go 100mm?" - Now we can calculate
31
+ 2. "turn(30) for how long to rotate 45°?" - Now we can calculate
32
+ 3. Planning becomes possible - decompose into known steps
33
+ 4. Drift detection - compare expected vs actual position
34
+ """
35
+
36
+ import time
37
+ import json
38
+ import math
39
+ from dataclasses import dataclass, asdict
40
+ from typing import Optional, Tuple, List, Callable
41
+ from pathlib import Path
42
+
43
+
44
+ @dataclass
45
+ class LocomotionParameters:
46
+ """
47
+ Characterized locomotion parameters for a robot.
48
+
49
+ These are measured empirically by commanding motion and
50
+ observing actual displacement via external reference (ArUco marker).
51
+ """
52
+ # Forward motion
53
+ forward_mm_per_sec_at_50: float = 0.0 # mm/s at move(50, 0)
54
+ forward_variance: float = 0.0 # Standard deviation in mm
55
+
56
+ # Backward motion (may differ from forward)
57
+ backward_mm_per_sec_at_50: float = 0.0
58
+
59
+ # Turn motion
60
+ turn_deg_per_sec_at_30: float = 0.0 # deg/s at move(0, 30)
61
+ turn_variance: float = 0.0 # Standard deviation in degrees
62
+
63
+ # Relationship (assumed linear for simplicity)
64
+ # actual_vel = command_vel * scale_factor
65
+ forward_scale: float = 1.0
66
+ turn_scale: float = 1.0
67
+
68
+ # Calibration metadata
69
+ surface_type: str = "unknown" # e.g., "hardwood", "carpet", "tile"
70
+ battery_level: Optional[float] = None # Battery % at calibration
71
+ calibrated_at: str = ""
72
+
73
+ def time_for_forward_mm(self, distance_mm: float, command_vel: int = 50) -> float:
74
+ """Calculate time needed to move forward given distance."""
75
+ if self.forward_mm_per_sec_at_50 <= 0:
76
+ raise ValueError("Locomotion not characterized - run characterize_locomotion() first")
77
+
78
+ # Scale velocity to match calibration baseline
79
+ vel_ratio = command_vel / 50.0
80
+ mm_per_sec = self.forward_mm_per_sec_at_50 * vel_ratio * self.forward_scale
81
+
82
+ return distance_mm / mm_per_sec
83
+
84
+ def time_for_turn_deg(self, angle_deg: float, command_vel: int = 30) -> float:
85
+ """Calculate time needed to turn given angle."""
86
+ if self.turn_deg_per_sec_at_30 <= 0:
87
+ raise ValueError("Locomotion not characterized - run characterize_locomotion() first")
88
+
89
+ vel_ratio = command_vel / 30.0
90
+ deg_per_sec = self.turn_deg_per_sec_at_30 * vel_ratio * self.turn_scale
91
+
92
+ return abs(angle_deg) / deg_per_sec
93
+
94
+ def to_dict(self) -> dict:
95
+ return asdict(self)
96
+
97
+ @classmethod
98
+ def from_dict(cls, data: dict) -> 'LocomotionParameters':
99
+ return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
100
+
101
+
102
+ class LocomotionCalibrator:
103
+ """
104
+ Characterizes locomotion parameters using visual reference.
105
+
106
+ Requires:
107
+ - Robot driver with move() command
108
+ - Camera that can see ArUco marker on ground
109
+ - ArUco marker placed on floor in robot's path
110
+
111
+ The process:
112
+ 1. Detect marker position (reference point)
113
+ 2. Command robot to move
114
+ 3. Detect new marker position
115
+ 4. Calculate actual displacement
116
+ 5. Store velocity-to-displacement relationship
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ driver,
122
+ get_frame: Callable,
123
+ marker_size_mm: float = 50.0,
124
+ marker_id: int = 0,
125
+ ):
126
+ """
127
+ Initialize locomotion calibrator.
128
+
129
+ Args:
130
+ driver: Robot driver with move() and stop() methods
131
+ get_frame: Function that returns camera frame (numpy array)
132
+ marker_size_mm: Physical size of ArUco marker
133
+ marker_id: ArUco marker ID to track
134
+ """
135
+ self.driver = driver
136
+ self.get_frame = get_frame
137
+ self.marker_size_mm = marker_size_mm
138
+ self.marker_id = marker_id
139
+
140
+ # ArUco detection setup
141
+ try:
142
+ import cv2
143
+ self._cv2 = cv2
144
+ self._aruco = cv2.aruco
145
+ self._aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
146
+ self._aruco_params = cv2.aruco.DetectorParameters()
147
+ self._detector = cv2.aruco.ArucoDetector(self._aruco_dict, self._aruco_params)
148
+ except ImportError:
149
+ raise ImportError("OpenCV with ArUco support required")
150
+
151
+ self.params = LocomotionParameters()
152
+
153
+ def detect_marker(self) -> Optional[Tuple[float, float, float]]:
154
+ """
155
+ Detect ArUco marker and return (x, y, angle) in image coordinates.
156
+
157
+ Returns:
158
+ (center_x, center_y, rotation_deg) or None if not found
159
+ """
160
+ frame = self.get_frame()
161
+ if frame is None:
162
+ return None
163
+
164
+ gray = self._cv2.cvtColor(frame, self._cv2.COLOR_BGR2GRAY)
165
+ corners, ids, rejected = self._detector.detectMarkers(gray)
166
+
167
+ if ids is None or self.marker_id not in ids.flatten():
168
+ return None
169
+
170
+ idx = list(ids.flatten()).index(self.marker_id)
171
+ corner = corners[idx][0]
172
+
173
+ # Calculate center
174
+ center_x = corner[:, 0].mean()
175
+ center_y = corner[:, 1].mean()
176
+
177
+ # Calculate rotation from marker orientation
178
+ dx = corner[1, 0] - corner[0, 0]
179
+ dy = corner[1, 1] - corner[0, 1]
180
+ angle = math.degrees(math.atan2(dy, dx))
181
+
182
+ return (center_x, center_y, angle)
183
+
184
+ def measure_marker_displacement(
185
+ self,
186
+ move_cmd: Tuple[int, int],
187
+ duration_sec: float,
188
+ ) -> Optional[Tuple[float, float, float]]:
189
+ """
190
+ Measure actual displacement from a move command.
191
+
192
+ Args:
193
+ move_cmd: (forward_vel, turn_vel) to send to robot
194
+ duration_sec: How long to move
195
+
196
+ Returns:
197
+ (delta_x_pixels, delta_y_pixels, delta_angle_deg) or None
198
+ """
199
+ # Get initial position
200
+ initial = self.detect_marker()
201
+ if initial is None:
202
+ print("ERROR: Cannot see marker before move")
203
+ return None
204
+
205
+ # Execute move
206
+ forward, turn = move_cmd
207
+ if hasattr(self.driver, '_send_command'):
208
+ self.driver._send_command(f"dog.move({forward}, {turn})", wait=0.1)
209
+ else:
210
+ self.driver.move(forward, turn)
211
+
212
+ time.sleep(duration_sec)
213
+
214
+ # Stop
215
+ if hasattr(self.driver, '_send_command'):
216
+ self.driver._send_command("dog.move(0, 0)", wait=0.1)
217
+ else:
218
+ self.driver.stop()
219
+
220
+ time.sleep(0.3) # Let robot settle
221
+
222
+ # Get final position
223
+ final = self.detect_marker()
224
+ if final is None:
225
+ print("ERROR: Cannot see marker after move")
226
+ return None
227
+
228
+ # Calculate displacement
229
+ dx = final[0] - initial[0]
230
+ dy = final[1] - initial[1]
231
+ dangle = final[2] - initial[2]
232
+
233
+ return (dx, dy, dangle)
234
+
235
+ def pixels_to_mm(self, pixels: float, frame) -> float:
236
+ """
237
+ Convert pixel displacement to mm using marker size as reference.
238
+
239
+ This requires seeing the marker to calibrate the pixel-to-mm ratio.
240
+ """
241
+ # Detect marker to get its size in pixels
242
+ gray = self._cv2.cvtColor(frame, self._cv2.COLOR_BGR2GRAY)
243
+ corners, ids, _ = self._detector.detectMarkers(gray)
244
+
245
+ if ids is None or self.marker_id not in ids.flatten():
246
+ raise ValueError("Marker not visible - cannot calibrate pixel scale")
247
+
248
+ idx = list(ids.flatten()).index(self.marker_id)
249
+ corner = corners[idx][0]
250
+
251
+ # Marker size in pixels (average of width and height)
252
+ marker_pixels = (
253
+ math.sqrt((corner[1, 0] - corner[0, 0])**2 + (corner[1, 1] - corner[0, 1])**2) +
254
+ math.sqrt((corner[2, 0] - corner[1, 0])**2 + (corner[2, 1] - corner[1, 1])**2)
255
+ ) / 2
256
+
257
+ # Pixels per mm
258
+ scale = marker_pixels / self.marker_size_mm
259
+
260
+ return pixels / scale
261
+
262
+ def characterize_forward(self, trials: int = 3) -> float:
263
+ """
264
+ Characterize forward motion.
265
+
266
+ Returns:
267
+ mm/sec at move(50, 0)
268
+ """
269
+ print("\n=== Characterizing Forward Motion ===")
270
+ print("Place ArUco marker on floor in front of robot")
271
+ print("Robot will move forward 3 times")
272
+
273
+ measurements = []
274
+
275
+ for trial in range(trials):
276
+ print(f"\nTrial {trial + 1}/{trials}")
277
+
278
+ # Get reference frame for pixel calibration
279
+ frame = self.get_frame()
280
+
281
+ # Move forward for 1 second at velocity 50
282
+ result = self.measure_marker_displacement((50, 0), 1.0)
283
+
284
+ if result is None:
285
+ print(f" Trial failed - marker lost")
286
+ continue
287
+
288
+ dx, dy, _ = result
289
+
290
+ # Convert to mm (marker moves opposite to robot)
291
+ displacement_mm = -self.pixels_to_mm(dy, frame) # Y in image = forward
292
+ measurements.append(displacement_mm)
293
+
294
+ print(f" Displacement: {displacement_mm:.1f} mm")
295
+
296
+ if not measurements:
297
+ raise RuntimeError("All trials failed")
298
+
299
+ avg = sum(measurements) / len(measurements)
300
+ variance = sum((m - avg)**2 for m in measurements) / len(measurements)
301
+
302
+ print(f"\nResult: {avg:.1f} mm/s (±{math.sqrt(variance):.1f})")
303
+
304
+ self.params.forward_mm_per_sec_at_50 = avg
305
+ self.params.forward_variance = math.sqrt(variance)
306
+
307
+ return avg
308
+
309
+ def characterize_turn(self, trials: int = 3) -> float:
310
+ """
311
+ Characterize turn motion.
312
+
313
+ Returns:
314
+ deg/sec at move(0, 30)
315
+ """
316
+ print("\n=== Characterizing Turn Motion ===")
317
+ print("Place ArUco marker on floor beside robot")
318
+ print("Robot will turn in place 3 times")
319
+
320
+ measurements = []
321
+
322
+ for trial in range(trials):
323
+ print(f"\nTrial {trial + 1}/{trials}")
324
+
325
+ # Turn for 1 second at velocity 30
326
+ result = self.measure_marker_displacement((0, 30), 1.0)
327
+
328
+ if result is None:
329
+ print(f" Trial failed - marker lost")
330
+ continue
331
+
332
+ _, _, dangle = result
333
+ measurements.append(abs(dangle))
334
+
335
+ print(f" Rotation: {abs(dangle):.1f} deg")
336
+
337
+ if not measurements:
338
+ raise RuntimeError("All trials failed")
339
+
340
+ avg = sum(measurements) / len(measurements)
341
+ variance = sum((m - avg)**2 for m in measurements) / len(measurements)
342
+
343
+ print(f"\nResult: {avg:.1f} deg/s (±{math.sqrt(variance):.1f})")
344
+
345
+ self.params.turn_deg_per_sec_at_30 = avg
346
+ self.params.turn_variance = math.sqrt(variance)
347
+
348
+ return avg
349
+
350
+ def characterize_locomotion(
351
+ self,
352
+ surface_type: str = "unknown"
353
+ ) -> LocomotionParameters:
354
+ """
355
+ Run full locomotion characterization.
356
+
357
+ Returns:
358
+ LocomotionParameters with measured values
359
+ """
360
+ print("\n" + "=" * 60)
361
+ print("LOCOMOTION CHARACTERIZATION")
362
+ print("=" * 60)
363
+ print(f"Surface: {surface_type}")
364
+ print("This process will move the robot and measure displacement")
365
+ print("Ensure ArUco marker is visible on floor")
366
+
367
+ self.characterize_forward()
368
+ self.characterize_turn()
369
+
370
+ self.params.surface_type = surface_type
371
+ self.params.calibrated_at = time.strftime("%Y-%m-%dT%H:%M:%S")
372
+
373
+ print("\n" + "=" * 60)
374
+ print("CHARACTERIZATION COMPLETE")
375
+ print("=" * 60)
376
+ print(f"Forward: {self.params.forward_mm_per_sec_at_50:.1f} mm/s at vel=50")
377
+ print(f"Turn: {self.params.turn_deg_per_sec_at_30:.1f} deg/s at vel=30")
378
+
379
+ return self.params
380
+
381
+ def save_to_calibration(self, calibration_path: Path):
382
+ """
383
+ Add locomotion parameters to existing calibration file.
384
+ """
385
+ if calibration_path.exists():
386
+ with open(calibration_path) as f:
387
+ data = json.load(f)
388
+ else:
389
+ data = {}
390
+
391
+ data["locomotion"] = self.params.to_dict()
392
+
393
+ with open(calibration_path, 'w') as f:
394
+ json.dump(data, f, indent=2)
395
+
396
+ print(f"Saved locomotion parameters to {calibration_path}")
397
+
398
+ @classmethod
399
+ def load_from_calibration(cls, calibration_path: Path) -> LocomotionParameters:
400
+ """
401
+ Load locomotion parameters from calibration file.
402
+ """
403
+ with open(calibration_path) as f:
404
+ data = json.load(f)
405
+
406
+ if "locomotion" not in data:
407
+ raise ValueError("Calibration file has no locomotion parameters")
408
+
409
+ return LocomotionParameters.from_dict(data["locomotion"])
410
+
411
+
412
+ # =============================================================================
413
+ # Convenience functions for ate CLI
414
+ # =============================================================================
415
+
416
+ def characterize_mechdog(
417
+ serial_port: str = "/dev/cu.usbserial-10",
418
+ webcam_index: int = 0,
419
+ marker_size_mm: float = 50.0,
420
+ surface_type: str = "desk",
421
+ ) -> LocomotionParameters:
422
+ """
423
+ Run locomotion characterization for MechDog.
424
+
425
+ Requires:
426
+ - MechDog connected via USB
427
+ - Webcam viewing ArUco marker on floor
428
+ - ArUco marker (ID 0, 50mm) placed in front of robot
429
+
430
+ Returns:
431
+ LocomotionParameters with measured values
432
+ """
433
+ import cv2
434
+ import serial
435
+
436
+ # Setup serial
437
+ print(f"Connecting to MechDog on {serial_port}...")
438
+ ser = serial.Serial(serial_port, 115200, timeout=1)
439
+ time.sleep(0.5)
440
+ ser.read(ser.in_waiting)
441
+
442
+ def cmd(command: str, wait: float = 0.5):
443
+ ser.write(f"{command}\r\n".encode())
444
+ time.sleep(wait)
445
+ return ser.read(ser.in_waiting).decode('utf-8', errors='ignore')
446
+
447
+ cmd("from HW_MechDog import MechDog", 1.5)
448
+ cmd("dog = MechDog()", 1.5)
449
+ print("Connected!")
450
+
451
+ # Setup webcam
452
+ print(f"Opening webcam {webcam_index}...")
453
+ cap = cv2.VideoCapture(webcam_index)
454
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
455
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
456
+ print("Ready!")
457
+
458
+ # Create simple driver wrapper
459
+ class SimpleDriver:
460
+ def _send_command(self, command, wait=0.1):
461
+ cmd(command, wait)
462
+
463
+ def get_frame():
464
+ for _ in range(2):
465
+ cap.read()
466
+ ret, frame = cap.read()
467
+ return frame if ret else None
468
+
469
+ # Run characterization
470
+ calibrator = LocomotionCalibrator(
471
+ SimpleDriver(),
472
+ get_frame,
473
+ marker_size_mm=marker_size_mm,
474
+ )
475
+
476
+ try:
477
+ params = calibrator.characterize_locomotion(surface_type)
478
+ return params
479
+ finally:
480
+ ser.close()
481
+ cap.release()
482
+
483
+
484
+ if __name__ == "__main__":
485
+ print("Locomotion Characterization Demo")
486
+ print("================================")
487
+ print()
488
+ print("This will characterize your MechDog's locomotion.")
489
+ print("Requirements:")
490
+ print(" 1. MechDog connected via USB")
491
+ print(" 2. Webcam viewing floor area")
492
+ print(" 3. ArUco marker (ID 0, 50mm) on floor")
493
+ print()
494
+
495
+ try:
496
+ params = characterize_mechdog()
497
+
498
+ print("\n" + "=" * 40)
499
+ print("To move forward 100mm:")
500
+ t = params.time_for_forward_mm(100)
501
+ print(f" dog.move(50, 0) for {t:.2f} seconds")
502
+
503
+ print("\nTo turn 45 degrees:")
504
+ t = params.time_for_turn_deg(45)
505
+ print(f" dog.move(0, 30) for {t:.2f} seconds")
506
+
507
+ except Exception as e:
508
+ print(f"Error: {e}")