foodforthought-cli 0.2.4__py3-none-any.whl → 0.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ate/__init__.py +1 -1
- ate/behaviors/__init__.py +88 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +610 -54
- ate/drivers/__init__.py +27 -0
- ate/drivers/mechdog.py +606 -0
- ate/interfaces/__init__.py +171 -0
- ate/interfaces/base.py +271 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/types.py +371 -0
- ate/mcp_server.py +387 -0
- ate/recording/__init__.py +44 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +405 -0
- ate/recording/upload.py +304 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +79 -0
- ate/robot/calibration.py +583 -0
- ate/robot/commands.py +3603 -0
- ate/robot/discovery.py +339 -0
- ate/robot/introspection.py +330 -0
- ate/robot/manager.py +270 -0
- ate/robot/profiles.py +275 -0
- ate/robot/registry.py +319 -0
- ate/robot/skill_upload.py +393 -0
- ate/robot/visual_labeler.py +1039 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +37 -8
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/top_level.txt +0 -0
ate/robot/calibration.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visual calibration system for robot servo mapping.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- ate robot calibrate - Interactive servo discovery and range mapping
|
|
6
|
+
- Visual feedback using webcam/robot camera
|
|
7
|
+
- Pose library management for semantic skill development
|
|
8
|
+
|
|
9
|
+
The goal is to eliminate manual servo value guessing by:
|
|
10
|
+
1. Auto-discovering servo IDs and their ranges
|
|
11
|
+
2. Using camera to visually confirm poses
|
|
12
|
+
3. Creating semantic mappings (e.g., "gripper_open" -> servo 11 @ 2500)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
import os
|
|
18
|
+
from dataclasses import dataclass, field, asdict
|
|
19
|
+
from typing import Dict, List, Optional, Tuple, Any, Callable
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from enum import Enum
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JointType(Enum):
|
|
25
|
+
"""Types of joints/servos."""
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
# Locomotion
|
|
28
|
+
HIP_ROLL = "hip_roll"
|
|
29
|
+
HIP_PITCH = "hip_pitch"
|
|
30
|
+
KNEE = "knee"
|
|
31
|
+
ANKLE = "ankle"
|
|
32
|
+
# Arm
|
|
33
|
+
SHOULDER_PAN = "shoulder_pan"
|
|
34
|
+
SHOULDER_LIFT = "shoulder_lift"
|
|
35
|
+
ELBOW = "elbow"
|
|
36
|
+
WRIST_ROLL = "wrist_roll"
|
|
37
|
+
WRIST_PITCH = "wrist_pitch"
|
|
38
|
+
# Gripper
|
|
39
|
+
GRIPPER = "gripper"
|
|
40
|
+
# Body
|
|
41
|
+
HEAD_PAN = "head_pan"
|
|
42
|
+
HEAD_TILT = "head_tilt"
|
|
43
|
+
BODY_PITCH = "body_pitch"
|
|
44
|
+
BODY_ROLL = "body_roll"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ServoCalibration:
|
|
49
|
+
"""Calibration data for a single servo."""
|
|
50
|
+
servo_id: int
|
|
51
|
+
name: str # Human-readable name
|
|
52
|
+
joint_type: JointType = JointType.UNKNOWN
|
|
53
|
+
|
|
54
|
+
# Range discovered during calibration
|
|
55
|
+
min_value: int = 0
|
|
56
|
+
max_value: int = 4096
|
|
57
|
+
center_value: int = 2048
|
|
58
|
+
|
|
59
|
+
# Semantic positions (name -> value)
|
|
60
|
+
positions: Dict[str, int] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
# Physical properties
|
|
63
|
+
inverted: bool = False # True if positive = clockwise
|
|
64
|
+
speed_limit: int = 1000 # Max speed in ms per movement
|
|
65
|
+
|
|
66
|
+
# Visual confirmation images (base64 or paths)
|
|
67
|
+
position_images: Dict[str, str] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def get_position(self, name: str) -> Optional[int]:
|
|
70
|
+
"""Get servo value for a named position."""
|
|
71
|
+
return self.positions.get(name)
|
|
72
|
+
|
|
73
|
+
def set_position(self, name: str, value: int, image_path: Optional[str] = None):
|
|
74
|
+
"""Set a named position for this servo."""
|
|
75
|
+
self.positions[name] = value
|
|
76
|
+
if image_path:
|
|
77
|
+
self.position_images[name] = image_path
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Pose:
|
|
82
|
+
"""A named pose consisting of multiple servo positions."""
|
|
83
|
+
name: str
|
|
84
|
+
description: str = ""
|
|
85
|
+
servo_positions: Dict[int, int] = field(default_factory=dict) # servo_id -> value
|
|
86
|
+
transition_time_ms: int = 500
|
|
87
|
+
image_path: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
return {
|
|
91
|
+
"name": self.name,
|
|
92
|
+
"description": self.description,
|
|
93
|
+
"servo_positions": self.servo_positions,
|
|
94
|
+
"transition_time_ms": self.transition_time_ms,
|
|
95
|
+
"image_path": self.image_path,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, data: dict) -> "Pose":
|
|
100
|
+
return cls(
|
|
101
|
+
name=data["name"],
|
|
102
|
+
description=data.get("description", ""),
|
|
103
|
+
servo_positions={int(k): v for k, v in data["servo_positions"].items()},
|
|
104
|
+
transition_time_ms=data.get("transition_time_ms", 500),
|
|
105
|
+
image_path=data.get("image_path"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class RobotCalibration:
|
|
111
|
+
"""Complete calibration for a robot."""
|
|
112
|
+
robot_model: str
|
|
113
|
+
robot_name: str
|
|
114
|
+
|
|
115
|
+
# Servo calibrations by ID
|
|
116
|
+
servos: Dict[int, ServoCalibration] = field(default_factory=dict)
|
|
117
|
+
|
|
118
|
+
# Named poses
|
|
119
|
+
poses: Dict[str, Pose] = field(default_factory=dict)
|
|
120
|
+
|
|
121
|
+
# Camera configuration
|
|
122
|
+
camera_url: Optional[str] = None
|
|
123
|
+
camera_type: str = "wifi" # wifi, usb, none
|
|
124
|
+
|
|
125
|
+
# Connection info
|
|
126
|
+
serial_port: Optional[str] = None
|
|
127
|
+
baud_rate: int = 115200
|
|
128
|
+
|
|
129
|
+
# Metadata
|
|
130
|
+
calibrated_at: Optional[str] = None
|
|
131
|
+
notes: str = ""
|
|
132
|
+
|
|
133
|
+
def get_servo(self, servo_id: int) -> Optional[ServoCalibration]:
|
|
134
|
+
return self.servos.get(servo_id)
|
|
135
|
+
|
|
136
|
+
def get_pose(self, name: str) -> Optional[Pose]:
|
|
137
|
+
return self.poses.get(name)
|
|
138
|
+
|
|
139
|
+
def add_pose(self, pose: Pose):
|
|
140
|
+
self.poses[pose.name] = pose
|
|
141
|
+
|
|
142
|
+
def save(self, path: Path):
|
|
143
|
+
"""Save calibration to JSON file."""
|
|
144
|
+
data = {
|
|
145
|
+
"robot_model": self.robot_model,
|
|
146
|
+
"robot_name": self.robot_name,
|
|
147
|
+
"servos": {
|
|
148
|
+
str(sid): {
|
|
149
|
+
"servo_id": s.servo_id,
|
|
150
|
+
"name": s.name,
|
|
151
|
+
"joint_type": s.joint_type.value,
|
|
152
|
+
"min_value": s.min_value,
|
|
153
|
+
"max_value": s.max_value,
|
|
154
|
+
"center_value": s.center_value,
|
|
155
|
+
"positions": s.positions,
|
|
156
|
+
"inverted": s.inverted,
|
|
157
|
+
"speed_limit": s.speed_limit,
|
|
158
|
+
}
|
|
159
|
+
for sid, s in self.servos.items()
|
|
160
|
+
},
|
|
161
|
+
"poses": {name: pose.to_dict() for name, pose in self.poses.items()},
|
|
162
|
+
"camera_url": self.camera_url,
|
|
163
|
+
"camera_type": self.camera_type,
|
|
164
|
+
"serial_port": self.serial_port,
|
|
165
|
+
"baud_rate": self.baud_rate,
|
|
166
|
+
"calibrated_at": self.calibrated_at,
|
|
167
|
+
"notes": self.notes,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
with open(path, "w") as f:
|
|
172
|
+
json.dump(data, f, indent=2)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def load(cls, path: Path) -> "RobotCalibration":
|
|
176
|
+
"""Load calibration from JSON file."""
|
|
177
|
+
with open(path) as f:
|
|
178
|
+
data = json.load(f)
|
|
179
|
+
|
|
180
|
+
cal = cls(
|
|
181
|
+
robot_model=data["robot_model"],
|
|
182
|
+
robot_name=data["robot_name"],
|
|
183
|
+
camera_url=data.get("camera_url"),
|
|
184
|
+
camera_type=data.get("camera_type", "wifi"),
|
|
185
|
+
serial_port=data.get("serial_port"),
|
|
186
|
+
baud_rate=data.get("baud_rate", 115200),
|
|
187
|
+
calibrated_at=data.get("calibrated_at"),
|
|
188
|
+
notes=data.get("notes", ""),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Load servos
|
|
192
|
+
for sid, sdata in data.get("servos", {}).items():
|
|
193
|
+
cal.servos[int(sid)] = ServoCalibration(
|
|
194
|
+
servo_id=sdata["servo_id"],
|
|
195
|
+
name=sdata["name"],
|
|
196
|
+
joint_type=JointType(sdata.get("joint_type", "unknown")),
|
|
197
|
+
min_value=sdata.get("min_value", 0),
|
|
198
|
+
max_value=sdata.get("max_value", 4096),
|
|
199
|
+
center_value=sdata.get("center_value", 2048),
|
|
200
|
+
positions=sdata.get("positions", {}),
|
|
201
|
+
inverted=sdata.get("inverted", False),
|
|
202
|
+
speed_limit=sdata.get("speed_limit", 1000),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Load poses
|
|
206
|
+
for name, pdata in data.get("poses", {}).items():
|
|
207
|
+
cal.poses[name] = Pose.from_dict(pdata)
|
|
208
|
+
|
|
209
|
+
return cal
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class VisualCalibrator:
|
|
213
|
+
"""
|
|
214
|
+
Interactive calibration wizard with visual feedback.
|
|
215
|
+
|
|
216
|
+
Usage:
|
|
217
|
+
calibrator = VisualCalibrator(serial_port="/dev/cu.usbserial-10")
|
|
218
|
+
calibrator.set_camera("http://192.168.50.98:80/capture")
|
|
219
|
+
calibration = calibrator.run_interactive()
|
|
220
|
+
calibration.save(Path("~/.ate/calibrations/mechdog.json"))
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
serial_port: str,
|
|
226
|
+
baud_rate: int = 115200,
|
|
227
|
+
robot_model: str = "unknown",
|
|
228
|
+
robot_name: str = "robot",
|
|
229
|
+
):
|
|
230
|
+
self.serial_port = serial_port
|
|
231
|
+
self.baud_rate = baud_rate
|
|
232
|
+
self.robot_model = robot_model
|
|
233
|
+
self.robot_name = robot_name
|
|
234
|
+
|
|
235
|
+
self._serial = None
|
|
236
|
+
self._camera_url = None
|
|
237
|
+
self._camera_capture = None # Function to capture image
|
|
238
|
+
|
|
239
|
+
self.calibration = RobotCalibration(
|
|
240
|
+
robot_model=robot_model,
|
|
241
|
+
robot_name=robot_name,
|
|
242
|
+
serial_port=serial_port,
|
|
243
|
+
baud_rate=baud_rate,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def set_camera(self, camera_url: str):
|
|
247
|
+
"""Set camera URL for visual feedback."""
|
|
248
|
+
self._camera_url = camera_url
|
|
249
|
+
self.calibration.camera_url = camera_url
|
|
250
|
+
|
|
251
|
+
def set_camera_capture(self, capture_fn: Callable[[], bytes]):
|
|
252
|
+
"""Set custom camera capture function."""
|
|
253
|
+
self._camera_capture = capture_fn
|
|
254
|
+
|
|
255
|
+
def connect(self) -> bool:
|
|
256
|
+
"""Connect to robot via serial."""
|
|
257
|
+
try:
|
|
258
|
+
import serial
|
|
259
|
+
self._serial = serial.Serial(self.serial_port, self.baud_rate, timeout=2.0)
|
|
260
|
+
time.sleep(0.5)
|
|
261
|
+
self._serial.reset_input_buffer()
|
|
262
|
+
|
|
263
|
+
# Get to REPL
|
|
264
|
+
self._serial.write(b'\x03')
|
|
265
|
+
time.sleep(0.3)
|
|
266
|
+
self._serial.write(b'\x02')
|
|
267
|
+
time.sleep(0.3)
|
|
268
|
+
self._serial.read(self._serial.in_waiting or 1)
|
|
269
|
+
|
|
270
|
+
return True
|
|
271
|
+
except Exception as e:
|
|
272
|
+
print(f"Failed to connect: {e}")
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
def disconnect(self):
|
|
276
|
+
"""Disconnect from robot."""
|
|
277
|
+
if self._serial and self._serial.is_open:
|
|
278
|
+
self._serial.close()
|
|
279
|
+
|
|
280
|
+
def send_command(self, cmd: str, wait: float = 0.3) -> str:
|
|
281
|
+
"""Send command and return response."""
|
|
282
|
+
if not self._serial:
|
|
283
|
+
raise RuntimeError("Not connected")
|
|
284
|
+
|
|
285
|
+
self._serial.write(f"{cmd}\r\n".encode())
|
|
286
|
+
time.sleep(wait)
|
|
287
|
+
response = self._serial.read(self._serial.in_waiting or 1)
|
|
288
|
+
return response.decode("utf-8", errors="replace")
|
|
289
|
+
|
|
290
|
+
def capture_image(self, save_path: Optional[str] = None) -> Optional[bytes]:
|
|
291
|
+
"""Capture image from camera."""
|
|
292
|
+
if self._camera_capture:
|
|
293
|
+
img_data = self._camera_capture()
|
|
294
|
+
elif self._camera_url:
|
|
295
|
+
import requests
|
|
296
|
+
try:
|
|
297
|
+
response = requests.get(self._camera_url, timeout=5)
|
|
298
|
+
if response.status_code == 200:
|
|
299
|
+
img_data = response.content
|
|
300
|
+
else:
|
|
301
|
+
return None
|
|
302
|
+
except Exception:
|
|
303
|
+
return None
|
|
304
|
+
else:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
if save_path and img_data:
|
|
308
|
+
with open(save_path, "wb") as f:
|
|
309
|
+
f.write(img_data)
|
|
310
|
+
|
|
311
|
+
return img_data
|
|
312
|
+
|
|
313
|
+
def read_servo(self, servo_id: int) -> Optional[int]:
|
|
314
|
+
"""Read current servo position."""
|
|
315
|
+
response = self.send_command(f"dog.read_servo({servo_id})")
|
|
316
|
+
# Parse response for integer value
|
|
317
|
+
for line in response.split("\n"):
|
|
318
|
+
line = line.strip()
|
|
319
|
+
if line.isdigit():
|
|
320
|
+
return int(line)
|
|
321
|
+
try:
|
|
322
|
+
return int(line)
|
|
323
|
+
except ValueError:
|
|
324
|
+
continue
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def set_servo(self, servo_id: int, value: int, time_ms: int = 500) -> bool:
|
|
328
|
+
"""Set servo to position."""
|
|
329
|
+
response = self.send_command(
|
|
330
|
+
f"dog.set_servo({servo_id}, {value}, {time_ms})",
|
|
331
|
+
wait=time_ms / 1000 + 0.3
|
|
332
|
+
)
|
|
333
|
+
return "True" in response
|
|
334
|
+
|
|
335
|
+
def discover_servos(self, max_id: int = 16) -> List[int]:
|
|
336
|
+
"""Discover which servo IDs are active."""
|
|
337
|
+
active = []
|
|
338
|
+
print(f"Scanning servos 1-{max_id}...")
|
|
339
|
+
|
|
340
|
+
for servo_id in range(1, max_id + 1):
|
|
341
|
+
pos = self.read_servo(servo_id)
|
|
342
|
+
if pos is not None:
|
|
343
|
+
active.append(servo_id)
|
|
344
|
+
print(f" Found servo {servo_id} at position {pos}")
|
|
345
|
+
|
|
346
|
+
return active
|
|
347
|
+
|
|
348
|
+
def calibrate_servo_range(
|
|
349
|
+
self,
|
|
350
|
+
servo_id: int,
|
|
351
|
+
name: str = "",
|
|
352
|
+
test_values: List[int] = None,
|
|
353
|
+
) -> ServoCalibration:
|
|
354
|
+
"""
|
|
355
|
+
Interactively calibrate a single servo.
|
|
356
|
+
|
|
357
|
+
Moves servo through range while user confirms safe limits.
|
|
358
|
+
"""
|
|
359
|
+
if test_values is None:
|
|
360
|
+
test_values = [500, 1000, 1500, 2000, 2500]
|
|
361
|
+
|
|
362
|
+
print(f"\n=== Calibrating Servo {servo_id} ===")
|
|
363
|
+
if not name:
|
|
364
|
+
name = input(f"Name for servo {servo_id} (e.g., 'gripper', 'arm_elbow'): ").strip()
|
|
365
|
+
if not name:
|
|
366
|
+
name = f"servo_{servo_id}"
|
|
367
|
+
|
|
368
|
+
cal = ServoCalibration(servo_id=servo_id, name=name)
|
|
369
|
+
|
|
370
|
+
# Test range
|
|
371
|
+
print(f"\nTesting positions: {test_values}")
|
|
372
|
+
print("Watch the servo and note safe range...")
|
|
373
|
+
|
|
374
|
+
working_values = []
|
|
375
|
+
for val in test_values:
|
|
376
|
+
print(f" Moving to {val}...", end=" ", flush=True)
|
|
377
|
+
success = self.set_servo(servo_id, val)
|
|
378
|
+
if success:
|
|
379
|
+
print("OK")
|
|
380
|
+
working_values.append(val)
|
|
381
|
+
else:
|
|
382
|
+
print("FAILED")
|
|
383
|
+
time.sleep(0.5)
|
|
384
|
+
|
|
385
|
+
if working_values:
|
|
386
|
+
cal.min_value = min(working_values)
|
|
387
|
+
cal.max_value = max(working_values)
|
|
388
|
+
cal.center_value = working_values[len(working_values) // 2]
|
|
389
|
+
print(f"\nRange: {cal.min_value} - {cal.max_value}")
|
|
390
|
+
|
|
391
|
+
return cal
|
|
392
|
+
|
|
393
|
+
def record_pose(self, name: str, description: str = "") -> Pose:
|
|
394
|
+
"""
|
|
395
|
+
Record current robot position as a named pose.
|
|
396
|
+
|
|
397
|
+
Reads all known servos and saves their positions.
|
|
398
|
+
"""
|
|
399
|
+
pose = Pose(name=name, description=description)
|
|
400
|
+
|
|
401
|
+
for servo_id in self.calibration.servos:
|
|
402
|
+
pos = self.read_servo(servo_id)
|
|
403
|
+
if pos is not None:
|
|
404
|
+
pose.servo_positions[servo_id] = pos
|
|
405
|
+
|
|
406
|
+
# Capture image if camera available
|
|
407
|
+
if self._camera_url:
|
|
408
|
+
img_dir = Path.home() / ".ate" / "calibrations" / "images"
|
|
409
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
|
410
|
+
img_path = img_dir / f"{self.robot_name}_{name}.jpg"
|
|
411
|
+
self.capture_image(str(img_path))
|
|
412
|
+
pose.image_path = str(img_path)
|
|
413
|
+
|
|
414
|
+
self.calibration.add_pose(pose)
|
|
415
|
+
return pose
|
|
416
|
+
|
|
417
|
+
def apply_pose(self, pose: Pose, sequential: bool = True) -> bool:
|
|
418
|
+
"""
|
|
419
|
+
Apply a saved pose to the robot.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
pose: The pose to apply
|
|
423
|
+
sequential: If True, move servos one at a time with waits
|
|
424
|
+
"""
|
|
425
|
+
success = True
|
|
426
|
+
|
|
427
|
+
for servo_id, value in pose.servo_positions.items():
|
|
428
|
+
result = self.set_servo(servo_id, value, pose.transition_time_ms)
|
|
429
|
+
if not result:
|
|
430
|
+
success = False
|
|
431
|
+
|
|
432
|
+
if sequential:
|
|
433
|
+
time.sleep(pose.transition_time_ms / 1000 + 0.2)
|
|
434
|
+
|
|
435
|
+
return success
|
|
436
|
+
|
|
437
|
+
def run_interactive(self) -> RobotCalibration:
|
|
438
|
+
"""
|
|
439
|
+
Run full interactive calibration wizard.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Complete RobotCalibration object
|
|
443
|
+
"""
|
|
444
|
+
from datetime import datetime
|
|
445
|
+
|
|
446
|
+
print("\n" + "=" * 50)
|
|
447
|
+
print(" FoodforThought Robot Calibration Wizard")
|
|
448
|
+
print("=" * 50)
|
|
449
|
+
|
|
450
|
+
# Connect
|
|
451
|
+
print("\nConnecting to robot...")
|
|
452
|
+
if not self.connect():
|
|
453
|
+
raise RuntimeError("Failed to connect to robot")
|
|
454
|
+
print("Connected!")
|
|
455
|
+
|
|
456
|
+
# Initialize robot
|
|
457
|
+
print("\nInitializing robot...")
|
|
458
|
+
self.send_command("from HW_MechDog import MechDog; dog = MechDog()", wait=1.5)
|
|
459
|
+
|
|
460
|
+
# Discover servos
|
|
461
|
+
print("\n--- Servo Discovery ---")
|
|
462
|
+
active_servos = self.discover_servos()
|
|
463
|
+
print(f"\nFound {len(active_servos)} active servos: {active_servos}")
|
|
464
|
+
|
|
465
|
+
# Calibrate each servo
|
|
466
|
+
print("\n--- Servo Calibration ---")
|
|
467
|
+
print("For each servo, we'll test its range.")
|
|
468
|
+
print("Press Enter to continue or 'skip' to skip a servo.\n")
|
|
469
|
+
|
|
470
|
+
for servo_id in active_servos:
|
|
471
|
+
skip = input(f"Calibrate servo {servo_id}? [Enter/skip]: ").strip().lower()
|
|
472
|
+
if skip == "skip":
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
cal = self.calibrate_servo_range(servo_id)
|
|
476
|
+
self.calibration.servos[servo_id] = cal
|
|
477
|
+
|
|
478
|
+
# Record key poses
|
|
479
|
+
print("\n--- Pose Recording ---")
|
|
480
|
+
print("Now let's record some key poses.")
|
|
481
|
+
print("Manually position the robot and type a pose name to save it.")
|
|
482
|
+
print("Type 'done' when finished.\n")
|
|
483
|
+
|
|
484
|
+
while True:
|
|
485
|
+
pose_name = input("Pose name (or 'done'): ").strip()
|
|
486
|
+
if pose_name.lower() == "done":
|
|
487
|
+
break
|
|
488
|
+
if not pose_name:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
desc = input("Description: ").strip()
|
|
492
|
+
pose = self.record_pose(pose_name, desc)
|
|
493
|
+
print(f"Recorded pose '{pose_name}' with {len(pose.servo_positions)} servos")
|
|
494
|
+
|
|
495
|
+
# Finalize
|
|
496
|
+
self.calibration.calibrated_at = datetime.now().isoformat()
|
|
497
|
+
self.disconnect()
|
|
498
|
+
|
|
499
|
+
print("\n" + "=" * 50)
|
|
500
|
+
print(" Calibration Complete!")
|
|
501
|
+
print("=" * 50)
|
|
502
|
+
print(f"Servos: {len(self.calibration.servos)}")
|
|
503
|
+
print(f"Poses: {len(self.calibration.poses)}")
|
|
504
|
+
|
|
505
|
+
return self.calibration
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# =============================================================================
|
|
509
|
+
# Quick calibration functions for common scenarios
|
|
510
|
+
# =============================================================================
|
|
511
|
+
|
|
512
|
+
def quick_gripper_calibration(
|
|
513
|
+
serial_port: str,
|
|
514
|
+
gripper_servo_id: int = 11,
|
|
515
|
+
test_open: int = 2500,
|
|
516
|
+
test_closed: int = 200,
|
|
517
|
+
) -> ServoCalibration:
|
|
518
|
+
"""
|
|
519
|
+
Quick calibration for just a gripper servo.
|
|
520
|
+
|
|
521
|
+
Usage:
|
|
522
|
+
gripper = quick_gripper_calibration("/dev/cu.usbserial-10")
|
|
523
|
+
print(f"Open: {gripper.positions['open']}, Closed: {gripper.positions['closed']}")
|
|
524
|
+
"""
|
|
525
|
+
calibrator = VisualCalibrator(serial_port)
|
|
526
|
+
|
|
527
|
+
if not calibrator.connect():
|
|
528
|
+
raise RuntimeError("Failed to connect")
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
calibrator.send_command("from HW_MechDog import MechDog; dog = MechDog()", wait=1.5)
|
|
532
|
+
|
|
533
|
+
gripper = ServoCalibration(
|
|
534
|
+
servo_id=gripper_servo_id,
|
|
535
|
+
name="gripper",
|
|
536
|
+
joint_type=JointType.GRIPPER,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Test open
|
|
540
|
+
print("Opening gripper...")
|
|
541
|
+
calibrator.set_servo(gripper_servo_id, test_open)
|
|
542
|
+
time.sleep(1.5)
|
|
543
|
+
actual_open = calibrator.read_servo(gripper_servo_id)
|
|
544
|
+
gripper.positions["open"] = actual_open or test_open
|
|
545
|
+
gripper.max_value = gripper.positions["open"]
|
|
546
|
+
|
|
547
|
+
# Test closed
|
|
548
|
+
print("Closing gripper...")
|
|
549
|
+
calibrator.set_servo(gripper_servo_id, test_closed)
|
|
550
|
+
time.sleep(1.5)
|
|
551
|
+
actual_closed = calibrator.read_servo(gripper_servo_id)
|
|
552
|
+
gripper.positions["closed"] = actual_closed or test_closed
|
|
553
|
+
gripper.min_value = gripper.positions["closed"]
|
|
554
|
+
|
|
555
|
+
# Center
|
|
556
|
+
gripper.center_value = (gripper.min_value + gripper.max_value) // 2
|
|
557
|
+
|
|
558
|
+
return gripper
|
|
559
|
+
finally:
|
|
560
|
+
calibrator.disconnect()
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def load_calibration(robot_name: str) -> Optional[RobotCalibration]:
|
|
564
|
+
"""Load calibration from default location."""
|
|
565
|
+
path = Path.home() / ".ate" / "calibrations" / f"{robot_name}.json"
|
|
566
|
+
if path.exists():
|
|
567
|
+
return RobotCalibration.load(path)
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def save_calibration(calibration: RobotCalibration):
|
|
572
|
+
"""Save calibration to default location."""
|
|
573
|
+
path = Path.home() / ".ate" / "calibrations" / f"{calibration.robot_name}.json"
|
|
574
|
+
calibration.save(path)
|
|
575
|
+
print(f"Saved calibration to: {path}")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def list_calibrations() -> List[str]:
|
|
579
|
+
"""List available calibrations."""
|
|
580
|
+
cal_dir = Path.home() / ".ate" / "calibrations"
|
|
581
|
+
if not cal_dir.exists():
|
|
582
|
+
return []
|
|
583
|
+
return [p.stem for p in cal_dir.glob("*.json")]
|