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
|
@@ -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}")
|