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
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visual Labeling System for Robot Skill Development.
|
|
3
|
+
|
|
4
|
+
Uses dual cameras (external webcam + robot camera) to interactively:
|
|
5
|
+
1. Discover and label servos by their physical effect
|
|
6
|
+
2. Record named poses with visual confirmation
|
|
7
|
+
3. Sequence poses into actions
|
|
8
|
+
4. Generate reusable skill code
|
|
9
|
+
|
|
10
|
+
This creates a "bedrock" of basic skills that can be:
|
|
11
|
+
- Reused across similar robots
|
|
12
|
+
- Composed into complex behaviors
|
|
13
|
+
- Shared in the FoodforThought marketplace
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Dict, List, Optional, Tuple, Callable, Any
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from enum import Enum
|
|
26
|
+
|
|
27
|
+
from .calibration import (
|
|
28
|
+
ServoCalibration,
|
|
29
|
+
Pose,
|
|
30
|
+
RobotCalibration,
|
|
31
|
+
JointType,
|
|
32
|
+
VisualCalibrator,
|
|
33
|
+
save_calibration,
|
|
34
|
+
load_calibration,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# =============================================================================
|
|
39
|
+
# Robot Preset Mappings - Pre-populated servo configurations
|
|
40
|
+
# =============================================================================
|
|
41
|
+
|
|
42
|
+
ROBOT_PRESETS = {
|
|
43
|
+
"hiwonder_mechdog": {
|
|
44
|
+
"name": "HiWonder MechDog",
|
|
45
|
+
"servos": {
|
|
46
|
+
# Front Right Leg
|
|
47
|
+
1: {"name": "front_right_hip", "joint_type": "hip_roll", "min": 500, "max": 2500},
|
|
48
|
+
2: {"name": "front_right_thigh", "joint_type": "hip_pitch", "min": 500, "max": 2500},
|
|
49
|
+
# Front Left Leg
|
|
50
|
+
3: {"name": "front_left_hip", "joint_type": "hip_roll", "min": 500, "max": 2500},
|
|
51
|
+
4: {"name": "front_left_thigh", "joint_type": "hip_pitch", "min": 500, "max": 2500},
|
|
52
|
+
# Rear Right Leg
|
|
53
|
+
5: {"name": "rear_right_hip", "joint_type": "hip_roll", "min": 500, "max": 2500},
|
|
54
|
+
6: {"name": "rear_right_thigh", "joint_type": "hip_pitch", "min": 500, "max": 2500},
|
|
55
|
+
# Rear Left Leg
|
|
56
|
+
7: {"name": "rear_left_hip", "joint_type": "hip_roll", "min": 500, "max": 2500},
|
|
57
|
+
8: {"name": "rear_left_thigh", "joint_type": "hip_pitch", "min": 500, "max": 2500},
|
|
58
|
+
# Arm (optional attachment)
|
|
59
|
+
9: {"name": "arm_shoulder", "joint_type": "shoulder_lift", "min": 500, "max": 2500},
|
|
60
|
+
10: {"name": "arm_elbow", "joint_type": "elbow", "min": 500, "max": 2500},
|
|
61
|
+
11: {"name": "gripper", "joint_type": "gripper", "min": 200, "max": 2500},
|
|
62
|
+
},
|
|
63
|
+
"joint_groups": {
|
|
64
|
+
"front_right_leg": [1, 2],
|
|
65
|
+
"front_left_leg": [3, 4],
|
|
66
|
+
"rear_right_leg": [5, 6],
|
|
67
|
+
"rear_left_leg": [7, 8],
|
|
68
|
+
"arm": [9, 10, 11],
|
|
69
|
+
"all_legs": [1, 2, 3, 4, 5, 6, 7, 8],
|
|
70
|
+
},
|
|
71
|
+
"default_poses": {
|
|
72
|
+
"stand": {1: 2096, 2: 1621, 3: 2170, 4: 1611, 5: 904, 6: 1379, 7: 830, 8: 1389},
|
|
73
|
+
"gripper_open": {11: 2500},
|
|
74
|
+
"gripper_closed": {11: 200},
|
|
75
|
+
"arm_up": {9: 500, 10: 1500},
|
|
76
|
+
"arm_down": {9: 1800, 10: 1200},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_preset(robot_model: str) -> Optional[Dict]:
|
|
83
|
+
"""Get preset mappings for a robot model."""
|
|
84
|
+
return ROBOT_PRESETS.get(robot_model)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def apply_preset_to_calibration(calibration: RobotCalibration, preset: Dict):
|
|
88
|
+
"""Apply preset servo mappings to a calibration."""
|
|
89
|
+
for servo_id, info in preset.get("servos", {}).items():
|
|
90
|
+
if servo_id not in calibration.servos:
|
|
91
|
+
jtype = JointType(info["joint_type"]) if info.get("joint_type") else JointType.UNKNOWN
|
|
92
|
+
calibration.servos[servo_id] = ServoCalibration(
|
|
93
|
+
servo_id=servo_id,
|
|
94
|
+
name=info["name"],
|
|
95
|
+
joint_type=jtype,
|
|
96
|
+
min_value=info.get("min", 500),
|
|
97
|
+
max_value=info.get("max", 2500),
|
|
98
|
+
center_value=(info.get("min", 500) + info.get("max", 2500)) // 2,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ActionType(Enum):
|
|
103
|
+
"""Types of robot actions."""
|
|
104
|
+
LOCOMOTION = "locomotion" # Walking, turning
|
|
105
|
+
MANIPULATION = "manipulation" # Picking, placing
|
|
106
|
+
GESTURE = "gesture" # Waving, nodding
|
|
107
|
+
POSTURE = "posture" # Standing, sitting
|
|
108
|
+
COMPOSITE = "composite" # Multi-step actions
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class ActionStep:
|
|
113
|
+
"""A single step in an action sequence."""
|
|
114
|
+
pose_name: str
|
|
115
|
+
duration_ms: int = 500
|
|
116
|
+
wait_after_ms: int = 0
|
|
117
|
+
description: str = ""
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict:
|
|
120
|
+
return {
|
|
121
|
+
"pose_name": self.pose_name,
|
|
122
|
+
"duration_ms": self.duration_ms,
|
|
123
|
+
"wait_after_ms": self.wait_after_ms,
|
|
124
|
+
"description": self.description,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_dict(cls, data: dict) -> "ActionStep":
|
|
129
|
+
return cls(**data)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class Action:
|
|
134
|
+
"""A named action consisting of pose sequences."""
|
|
135
|
+
name: str
|
|
136
|
+
action_type: ActionType = ActionType.COMPOSITE
|
|
137
|
+
description: str = ""
|
|
138
|
+
steps: List[ActionStep] = field(default_factory=list)
|
|
139
|
+
|
|
140
|
+
# Visual recordings
|
|
141
|
+
preview_image: Optional[str] = None # Path to preview image
|
|
142
|
+
video_path: Optional[str] = None # Path to recorded video
|
|
143
|
+
|
|
144
|
+
# Metadata
|
|
145
|
+
created_at: Optional[str] = None
|
|
146
|
+
tags: List[str] = field(default_factory=list)
|
|
147
|
+
|
|
148
|
+
def add_step(self, pose_name: str, duration_ms: int = 500, wait_after_ms: int = 0):
|
|
149
|
+
self.steps.append(ActionStep(pose_name, duration_ms, wait_after_ms))
|
|
150
|
+
|
|
151
|
+
def to_dict(self) -> dict:
|
|
152
|
+
return {
|
|
153
|
+
"name": self.name,
|
|
154
|
+
"action_type": self.action_type.value,
|
|
155
|
+
"description": self.description,
|
|
156
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
157
|
+
"preview_image": self.preview_image,
|
|
158
|
+
"video_path": self.video_path,
|
|
159
|
+
"created_at": self.created_at,
|
|
160
|
+
"tags": self.tags,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_dict(cls, data: dict) -> "Action":
|
|
165
|
+
action = cls(
|
|
166
|
+
name=data["name"],
|
|
167
|
+
action_type=ActionType(data.get("action_type", "composite")),
|
|
168
|
+
description=data.get("description", ""),
|
|
169
|
+
preview_image=data.get("preview_image"),
|
|
170
|
+
video_path=data.get("video_path"),
|
|
171
|
+
created_at=data.get("created_at"),
|
|
172
|
+
tags=data.get("tags", []),
|
|
173
|
+
)
|
|
174
|
+
for step_data in data.get("steps", []):
|
|
175
|
+
action.steps.append(ActionStep.from_dict(step_data))
|
|
176
|
+
return action
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class SkillLibrary:
|
|
181
|
+
"""Collection of labeled servos, poses, and actions for a robot."""
|
|
182
|
+
robot_name: str
|
|
183
|
+
robot_model: str
|
|
184
|
+
|
|
185
|
+
# Core data
|
|
186
|
+
calibration: Optional[RobotCalibration] = None
|
|
187
|
+
actions: Dict[str, Action] = field(default_factory=dict)
|
|
188
|
+
|
|
189
|
+
# Semantic mappings
|
|
190
|
+
servo_labels: Dict[int, str] = field(default_factory=dict) # servo_id -> description
|
|
191
|
+
joint_groups: Dict[str, List[int]] = field(default_factory=dict) # group_name -> servo_ids
|
|
192
|
+
|
|
193
|
+
# Metadata
|
|
194
|
+
created_at: Optional[str] = None
|
|
195
|
+
updated_at: Optional[str] = None
|
|
196
|
+
|
|
197
|
+
def save(self, path: Path):
|
|
198
|
+
"""Save skill library to JSON."""
|
|
199
|
+
data = {
|
|
200
|
+
"robot_name": self.robot_name,
|
|
201
|
+
"robot_model": self.robot_model,
|
|
202
|
+
"servo_labels": self.servo_labels,
|
|
203
|
+
"joint_groups": self.joint_groups,
|
|
204
|
+
"actions": {name: a.to_dict() for name, a in self.actions.items()},
|
|
205
|
+
"created_at": self.created_at,
|
|
206
|
+
"updated_at": datetime.now().isoformat(),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
with open(path, "w") as f:
|
|
211
|
+
json.dump(data, f, indent=2)
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def load(cls, path: Path, calibration: Optional[RobotCalibration] = None) -> "SkillLibrary":
|
|
215
|
+
"""Load skill library from JSON."""
|
|
216
|
+
with open(path) as f:
|
|
217
|
+
data = json.load(f)
|
|
218
|
+
|
|
219
|
+
lib = cls(
|
|
220
|
+
robot_name=data["robot_name"],
|
|
221
|
+
robot_model=data["robot_model"],
|
|
222
|
+
calibration=calibration,
|
|
223
|
+
servo_labels=data.get("servo_labels", {}),
|
|
224
|
+
joint_groups=data.get("joint_groups", {}),
|
|
225
|
+
created_at=data.get("created_at"),
|
|
226
|
+
updated_at=data.get("updated_at"),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
for name, action_data in data.get("actions", {}).items():
|
|
230
|
+
lib.actions[name] = Action.from_dict(action_data)
|
|
231
|
+
|
|
232
|
+
return lib
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class WebcamCapture:
|
|
236
|
+
"""Captures frames from local webcam using OpenCV."""
|
|
237
|
+
|
|
238
|
+
def __init__(self, device_id: int = 0):
|
|
239
|
+
self.device_id = device_id
|
|
240
|
+
self._cap = None
|
|
241
|
+
self._frame = None
|
|
242
|
+
self._running = False
|
|
243
|
+
self._thread = None
|
|
244
|
+
|
|
245
|
+
def start(self) -> bool:
|
|
246
|
+
"""Start webcam capture."""
|
|
247
|
+
try:
|
|
248
|
+
import cv2
|
|
249
|
+
self._cap = cv2.VideoCapture(self.device_id)
|
|
250
|
+
if not self._cap.isOpened():
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
self._running = True
|
|
254
|
+
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
|
|
255
|
+
self._thread.start()
|
|
256
|
+
return True
|
|
257
|
+
except ImportError:
|
|
258
|
+
print("OpenCV not installed. Run: pip install opencv-python")
|
|
259
|
+
return False
|
|
260
|
+
except Exception as e:
|
|
261
|
+
print(f"Failed to start webcam: {e}")
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
def _capture_loop(self):
|
|
265
|
+
"""Background capture loop."""
|
|
266
|
+
import cv2
|
|
267
|
+
while self._running:
|
|
268
|
+
ret, frame = self._cap.read()
|
|
269
|
+
if ret:
|
|
270
|
+
self._frame = frame
|
|
271
|
+
time.sleep(0.033) # ~30fps
|
|
272
|
+
|
|
273
|
+
def stop(self):
|
|
274
|
+
"""Stop webcam capture."""
|
|
275
|
+
self._running = False
|
|
276
|
+
if self._thread:
|
|
277
|
+
self._thread.join(timeout=1.0)
|
|
278
|
+
if self._cap:
|
|
279
|
+
self._cap.release()
|
|
280
|
+
|
|
281
|
+
def get_frame(self):
|
|
282
|
+
"""Get latest frame as numpy array."""
|
|
283
|
+
return self._frame
|
|
284
|
+
|
|
285
|
+
def capture_image(self, save_path: Optional[str] = None) -> Optional[bytes]:
|
|
286
|
+
"""Capture current frame as JPEG bytes."""
|
|
287
|
+
import cv2
|
|
288
|
+
|
|
289
|
+
if self._frame is None:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
_, jpeg = cv2.imencode('.jpg', self._frame)
|
|
293
|
+
img_bytes = jpeg.tobytes()
|
|
294
|
+
|
|
295
|
+
if save_path:
|
|
296
|
+
with open(save_path, 'wb') as f:
|
|
297
|
+
f.write(img_bytes)
|
|
298
|
+
|
|
299
|
+
return img_bytes
|
|
300
|
+
|
|
301
|
+
def show_preview(self, window_name: str = "Webcam"):
|
|
302
|
+
"""Show live preview window."""
|
|
303
|
+
import cv2
|
|
304
|
+
|
|
305
|
+
if self._frame is not None:
|
|
306
|
+
cv2.imshow(window_name, self._frame)
|
|
307
|
+
cv2.waitKey(1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class DualCameraLabeler:
|
|
311
|
+
"""
|
|
312
|
+
Interactive labeling system using dual cameras.
|
|
313
|
+
|
|
314
|
+
- External webcam: See the robot's body from outside
|
|
315
|
+
- Robot camera: See what the robot sees (for manipulation)
|
|
316
|
+
|
|
317
|
+
Workflow:
|
|
318
|
+
1. Discover servos and label by physical effect
|
|
319
|
+
2. Record named poses with dual-camera snapshots
|
|
320
|
+
3. Sequence poses into named actions
|
|
321
|
+
4. Generate Python skill code
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(
|
|
325
|
+
self,
|
|
326
|
+
serial_port: str,
|
|
327
|
+
robot_name: str = "robot",
|
|
328
|
+
robot_model: str = "unknown",
|
|
329
|
+
webcam_id: int = 0,
|
|
330
|
+
robot_camera_url: Optional[str] = None,
|
|
331
|
+
):
|
|
332
|
+
self.serial_port = serial_port
|
|
333
|
+
self.robot_name = robot_name
|
|
334
|
+
self.robot_model = robot_model
|
|
335
|
+
|
|
336
|
+
# Cameras
|
|
337
|
+
self.webcam = WebcamCapture(webcam_id)
|
|
338
|
+
self.robot_camera_url = robot_camera_url
|
|
339
|
+
|
|
340
|
+
# Core components
|
|
341
|
+
self.calibrator = VisualCalibrator(
|
|
342
|
+
serial_port=serial_port,
|
|
343
|
+
robot_model=robot_model,
|
|
344
|
+
robot_name=robot_name,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if robot_camera_url:
|
|
348
|
+
self.calibrator.set_camera(robot_camera_url)
|
|
349
|
+
|
|
350
|
+
# Skill library
|
|
351
|
+
self.library = SkillLibrary(
|
|
352
|
+
robot_name=robot_name,
|
|
353
|
+
robot_model=robot_model,
|
|
354
|
+
created_at=datetime.now().isoformat(),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# State
|
|
358
|
+
self._connected = False
|
|
359
|
+
self._display_thread = None
|
|
360
|
+
self._display_running = False
|
|
361
|
+
|
|
362
|
+
def connect(self) -> bool:
|
|
363
|
+
"""Connect to robot and start cameras."""
|
|
364
|
+
print("Connecting to robot...")
|
|
365
|
+
if not self.calibrator.connect():
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
# Initialize robot
|
|
369
|
+
self.calibrator.send_command("from HW_MechDog import MechDog; dog = MechDog()", wait=1.5)
|
|
370
|
+
|
|
371
|
+
print("Starting webcam...")
|
|
372
|
+
if not self.webcam.start():
|
|
373
|
+
print("Warning: Webcam not available")
|
|
374
|
+
|
|
375
|
+
self._connected = True
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
def disconnect(self):
|
|
379
|
+
"""Disconnect and cleanup."""
|
|
380
|
+
self._display_running = False
|
|
381
|
+
self.webcam.stop()
|
|
382
|
+
self.calibrator.disconnect()
|
|
383
|
+
self._connected = False
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
import cv2
|
|
387
|
+
cv2.destroyAllWindows()
|
|
388
|
+
except:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
def capture_dual_images(self, base_name: str) -> Tuple[Optional[str], Optional[str]]:
|
|
392
|
+
"""Capture images from both cameras."""
|
|
393
|
+
img_dir = Path.home() / ".ate" / "skill_images" / self.robot_name
|
|
394
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
|
395
|
+
|
|
396
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
397
|
+
|
|
398
|
+
# Webcam (external view)
|
|
399
|
+
webcam_path = img_dir / f"{base_name}_external_{timestamp}.jpg"
|
|
400
|
+
webcam_img = self.webcam.capture_image(str(webcam_path))
|
|
401
|
+
|
|
402
|
+
# Robot camera (robot's view)
|
|
403
|
+
robot_path = img_dir / f"{base_name}_robot_{timestamp}.jpg"
|
|
404
|
+
robot_img = self.calibrator.capture_image(str(robot_path))
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
str(webcam_path) if webcam_img else None,
|
|
408
|
+
str(robot_path) if robot_img else None,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def label_servo_interactive(self, servo_id: int) -> ServoCalibration:
|
|
412
|
+
"""
|
|
413
|
+
Interactively label a servo by moving it and observing the effect.
|
|
414
|
+
|
|
415
|
+
Shows live webcam feed while user adjusts servo position.
|
|
416
|
+
"""
|
|
417
|
+
print(f"\n=== Labeling Servo {servo_id} ===")
|
|
418
|
+
print("Watch the webcam window to see what this servo controls.")
|
|
419
|
+
print("Commands: +/- (adjust), min/max (extremes), name (set name), done (finish)")
|
|
420
|
+
|
|
421
|
+
# Start with center position
|
|
422
|
+
current_value = 1500
|
|
423
|
+
self.calibrator.set_servo(servo_id, current_value, 300)
|
|
424
|
+
|
|
425
|
+
cal = ServoCalibration(servo_id=servo_id, name=f"servo_{servo_id}")
|
|
426
|
+
tested_values = [current_value]
|
|
427
|
+
|
|
428
|
+
while True:
|
|
429
|
+
# Show webcam
|
|
430
|
+
self.webcam.show_preview(f"Servo {servo_id} - Webcam View")
|
|
431
|
+
|
|
432
|
+
cmd = input(f"Servo {servo_id} @ {current_value} > ").strip().lower()
|
|
433
|
+
|
|
434
|
+
if cmd in ('+', 'up', 'u'):
|
|
435
|
+
current_value = min(current_value + 200, 2500)
|
|
436
|
+
self.calibrator.set_servo(servo_id, current_value, 300)
|
|
437
|
+
tested_values.append(current_value)
|
|
438
|
+
|
|
439
|
+
elif cmd in ('-', 'down', 'd'):
|
|
440
|
+
current_value = max(current_value - 200, 200)
|
|
441
|
+
self.calibrator.set_servo(servo_id, current_value, 300)
|
|
442
|
+
tested_values.append(current_value)
|
|
443
|
+
|
|
444
|
+
elif cmd == 'min':
|
|
445
|
+
current_value = 200
|
|
446
|
+
self.calibrator.set_servo(servo_id, current_value, 500)
|
|
447
|
+
tested_values.append(current_value)
|
|
448
|
+
|
|
449
|
+
elif cmd == 'max':
|
|
450
|
+
current_value = 2500
|
|
451
|
+
self.calibrator.set_servo(servo_id, current_value, 500)
|
|
452
|
+
tested_values.append(current_value)
|
|
453
|
+
|
|
454
|
+
elif cmd.startswith('='):
|
|
455
|
+
try:
|
|
456
|
+
current_value = int(cmd[1:])
|
|
457
|
+
self.calibrator.set_servo(servo_id, current_value, 300)
|
|
458
|
+
tested_values.append(current_value)
|
|
459
|
+
except ValueError:
|
|
460
|
+
print("Invalid value. Use =1500 format.")
|
|
461
|
+
|
|
462
|
+
elif cmd == 'name' or cmd.startswith('name '):
|
|
463
|
+
name = cmd[5:].strip() if cmd.startswith('name ') else input("Name: ").strip()
|
|
464
|
+
if name:
|
|
465
|
+
cal.name = name
|
|
466
|
+
print(f"Named: {name}")
|
|
467
|
+
|
|
468
|
+
elif cmd == 'type':
|
|
469
|
+
print("Joint types:", [jt.value for jt in JointType])
|
|
470
|
+
jt = input("Joint type: ").strip()
|
|
471
|
+
try:
|
|
472
|
+
cal.joint_type = JointType(jt)
|
|
473
|
+
print(f"Set type: {cal.joint_type.value}")
|
|
474
|
+
except ValueError:
|
|
475
|
+
print("Invalid type")
|
|
476
|
+
|
|
477
|
+
elif cmd == 'snap':
|
|
478
|
+
ext_path, robot_path = self.capture_dual_images(f"servo_{servo_id}")
|
|
479
|
+
print(f"Captured: {ext_path}")
|
|
480
|
+
|
|
481
|
+
elif cmd == 'pos':
|
|
482
|
+
pos_name = input("Position name: ").strip()
|
|
483
|
+
if pos_name:
|
|
484
|
+
cal.positions[pos_name] = current_value
|
|
485
|
+
print(f"Saved position '{pos_name}' = {current_value}")
|
|
486
|
+
|
|
487
|
+
elif cmd in ('done', 'q', 'quit'):
|
|
488
|
+
break
|
|
489
|
+
|
|
490
|
+
elif cmd == 'help' or cmd == '?':
|
|
491
|
+
print("""
|
|
492
|
+
Commands:
|
|
493
|
+
+/- Adjust by 200
|
|
494
|
+
=VALUE Set to exact value (e.g., =1500)
|
|
495
|
+
min/max Go to extremes
|
|
496
|
+
name X Set servo name
|
|
497
|
+
type Set joint type
|
|
498
|
+
pos Save current as named position
|
|
499
|
+
snap Capture image
|
|
500
|
+
done Finish labeling
|
|
501
|
+
""")
|
|
502
|
+
|
|
503
|
+
# Calculate range from tested values
|
|
504
|
+
cal.min_value = min(tested_values)
|
|
505
|
+
cal.max_value = max(tested_values)
|
|
506
|
+
cal.center_value = (cal.min_value + cal.max_value) // 2
|
|
507
|
+
|
|
508
|
+
return cal
|
|
509
|
+
|
|
510
|
+
def record_pose_interactive(self) -> Optional[Pose]:
|
|
511
|
+
"""
|
|
512
|
+
Record current robot position as a named pose.
|
|
513
|
+
|
|
514
|
+
Shows both camera views for confirmation.
|
|
515
|
+
"""
|
|
516
|
+
print("\n=== Record Pose ===")
|
|
517
|
+
print("Position the robot manually or use servo commands first.")
|
|
518
|
+
|
|
519
|
+
# Show current state
|
|
520
|
+
self.webcam.show_preview("Current Pose - External")
|
|
521
|
+
|
|
522
|
+
name = input("Pose name: ").strip()
|
|
523
|
+
if not name:
|
|
524
|
+
print("Cancelled")
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
desc = input("Description: ").strip()
|
|
528
|
+
|
|
529
|
+
# Create pose from current servo positions
|
|
530
|
+
pose = Pose(name=name, description=desc)
|
|
531
|
+
|
|
532
|
+
for servo_id in self.calibrator.calibration.servos:
|
|
533
|
+
pos = self.calibrator.read_servo(servo_id)
|
|
534
|
+
if pos is not None:
|
|
535
|
+
pose.servo_positions[servo_id] = pos
|
|
536
|
+
|
|
537
|
+
# Capture images
|
|
538
|
+
ext_path, robot_path = self.capture_dual_images(f"pose_{name}")
|
|
539
|
+
pose.image_path = ext_path
|
|
540
|
+
|
|
541
|
+
# Add to calibration
|
|
542
|
+
self.calibrator.calibration.add_pose(pose)
|
|
543
|
+
|
|
544
|
+
print(f"Recorded pose '{name}' with {len(pose.servo_positions)} servos")
|
|
545
|
+
return pose
|
|
546
|
+
|
|
547
|
+
def create_action_interactive(self) -> Optional[Action]:
|
|
548
|
+
"""
|
|
549
|
+
Create an action by sequencing poses.
|
|
550
|
+
|
|
551
|
+
User selects poses and timing to create a multi-step action.
|
|
552
|
+
"""
|
|
553
|
+
print("\n=== Create Action ===")
|
|
554
|
+
|
|
555
|
+
# Show available poses
|
|
556
|
+
poses = list(self.calibrator.calibration.poses.keys())
|
|
557
|
+
if not poses:
|
|
558
|
+
print("No poses recorded. Record some poses first.")
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
print(f"Available poses: {poses}")
|
|
562
|
+
|
|
563
|
+
name = input("Action name: ").strip()
|
|
564
|
+
if not name:
|
|
565
|
+
print("Cancelled")
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
desc = input("Description: ").strip()
|
|
569
|
+
|
|
570
|
+
# Select action type
|
|
571
|
+
print(f"Action types: {[at.value for at in ActionType]}")
|
|
572
|
+
action_type_str = input("Action type [composite]: ").strip() or "composite"
|
|
573
|
+
try:
|
|
574
|
+
action_type = ActionType(action_type_str)
|
|
575
|
+
except ValueError:
|
|
576
|
+
action_type = ActionType.COMPOSITE
|
|
577
|
+
|
|
578
|
+
action = Action(
|
|
579
|
+
name=name,
|
|
580
|
+
action_type=action_type,
|
|
581
|
+
description=desc,
|
|
582
|
+
created_at=datetime.now().isoformat(),
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
print("\nAdd steps (pose names). Type 'done' when finished.")
|
|
586
|
+
print("Format: pose_name [duration_ms] [wait_after_ms]")
|
|
587
|
+
|
|
588
|
+
while True:
|
|
589
|
+
step_input = input(f"Step {len(action.steps) + 1}: ").strip()
|
|
590
|
+
|
|
591
|
+
if step_input.lower() in ('done', 'q', ''):
|
|
592
|
+
break
|
|
593
|
+
|
|
594
|
+
parts = step_input.split()
|
|
595
|
+
pose_name = parts[0]
|
|
596
|
+
|
|
597
|
+
if pose_name not in poses:
|
|
598
|
+
print(f"Unknown pose: {pose_name}")
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
duration = int(parts[1]) if len(parts) > 1 else 500
|
|
602
|
+
wait = int(parts[2]) if len(parts) > 2 else 0
|
|
603
|
+
|
|
604
|
+
action.add_step(pose_name, duration, wait)
|
|
605
|
+
print(f" Added: {pose_name} ({duration}ms, wait {wait}ms)")
|
|
606
|
+
|
|
607
|
+
if not action.steps:
|
|
608
|
+
print("No steps added, cancelled")
|
|
609
|
+
return None
|
|
610
|
+
|
|
611
|
+
# Preview action
|
|
612
|
+
preview = input("Preview action? [Y/n]: ").strip().lower()
|
|
613
|
+
if preview != 'n':
|
|
614
|
+
self.execute_action(action)
|
|
615
|
+
|
|
616
|
+
# Save
|
|
617
|
+
self.library.actions[name] = action
|
|
618
|
+
print(f"Created action '{name}' with {len(action.steps)} steps")
|
|
619
|
+
|
|
620
|
+
return action
|
|
621
|
+
|
|
622
|
+
def execute_action(self, action: Action):
|
|
623
|
+
"""Execute an action by applying its pose sequence."""
|
|
624
|
+
print(f"\nExecuting action: {action.name}")
|
|
625
|
+
|
|
626
|
+
for i, step in enumerate(action.steps):
|
|
627
|
+
pose = self.calibrator.calibration.get_pose(step.pose_name)
|
|
628
|
+
if not pose:
|
|
629
|
+
print(f" Warning: Pose '{step.pose_name}' not found")
|
|
630
|
+
continue
|
|
631
|
+
|
|
632
|
+
print(f" Step {i+1}: {step.pose_name}")
|
|
633
|
+
|
|
634
|
+
# Apply pose
|
|
635
|
+
for servo_id, value in pose.servo_positions.items():
|
|
636
|
+
self.calibrator.set_servo(servo_id, value, step.duration_ms)
|
|
637
|
+
|
|
638
|
+
# Wait for movement to complete
|
|
639
|
+
time.sleep(step.duration_ms / 1000 + 0.1)
|
|
640
|
+
|
|
641
|
+
# Additional wait if specified
|
|
642
|
+
if step.wait_after_ms > 0:
|
|
643
|
+
time.sleep(step.wait_after_ms / 1000)
|
|
644
|
+
|
|
645
|
+
print("Action complete")
|
|
646
|
+
|
|
647
|
+
def generate_skill_code(self, action: Action) -> str:
|
|
648
|
+
"""
|
|
649
|
+
Generate Python skill code from an action.
|
|
650
|
+
|
|
651
|
+
Creates a function that can be used as a reusable skill.
|
|
652
|
+
"""
|
|
653
|
+
func_name = action.name.lower().replace(" ", "_").replace("-", "_")
|
|
654
|
+
|
|
655
|
+
code = f'''"""
|
|
656
|
+
Auto-generated skill: {action.name}
|
|
657
|
+
Type: {action.action_type.value}
|
|
658
|
+
Description: {action.description}
|
|
659
|
+
Generated: {datetime.now().isoformat()}
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
from typing import Optional
|
|
663
|
+
from ate.interfaces import RobotInterface
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def {func_name}(robot: RobotInterface, speed_factor: float = 1.0) -> bool:
|
|
667
|
+
"""
|
|
668
|
+
{action.description or action.name}
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
robot: Robot interface with set_servo method
|
|
672
|
+
speed_factor: Multiplier for movement speed (1.0 = normal)
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
True if action completed successfully
|
|
676
|
+
"""
|
|
677
|
+
import time
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
'''
|
|
681
|
+
|
|
682
|
+
for i, step in enumerate(action.steps):
|
|
683
|
+
pose = self.calibrator.calibration.get_pose(step.pose_name)
|
|
684
|
+
if not pose:
|
|
685
|
+
continue
|
|
686
|
+
|
|
687
|
+
code += f'''
|
|
688
|
+
# Step {i+1}: {step.pose_name}
|
|
689
|
+
'''
|
|
690
|
+
for servo_id, value in pose.servo_positions.items():
|
|
691
|
+
servo_name = self.calibrator.calibration.servos.get(servo_id)
|
|
692
|
+
name_comment = f" # {servo_name.name}" if servo_name else ""
|
|
693
|
+
code += f''' robot.set_servo({servo_id}, {value}, int({step.duration_ms} / speed_factor)){name_comment}
|
|
694
|
+
'''
|
|
695
|
+
|
|
696
|
+
code += f''' time.sleep({step.duration_ms / 1000} / speed_factor)
|
|
697
|
+
'''
|
|
698
|
+
|
|
699
|
+
if step.wait_after_ms > 0:
|
|
700
|
+
code += f''' time.sleep({step.wait_after_ms / 1000})
|
|
701
|
+
'''
|
|
702
|
+
|
|
703
|
+
code += '''
|
|
704
|
+
return True
|
|
705
|
+
|
|
706
|
+
except Exception as e:
|
|
707
|
+
print(f"Skill failed: {e}")
|
|
708
|
+
return False
|
|
709
|
+
'''
|
|
710
|
+
|
|
711
|
+
return code
|
|
712
|
+
|
|
713
|
+
def run_interactive_session(self):
|
|
714
|
+
"""
|
|
715
|
+
Run a full interactive labeling session.
|
|
716
|
+
|
|
717
|
+
Main workflow:
|
|
718
|
+
1. Discover and label servos
|
|
719
|
+
2. Define joint groups
|
|
720
|
+
3. Record poses
|
|
721
|
+
4. Create actions
|
|
722
|
+
5. Generate skills
|
|
723
|
+
"""
|
|
724
|
+
print("\n" + "=" * 60)
|
|
725
|
+
print(" FoodforThought Visual Skill Labeler")
|
|
726
|
+
print("=" * 60)
|
|
727
|
+
print(f"\nRobot: {self.robot_name} ({self.robot_model})")
|
|
728
|
+
print(f"Serial: {self.serial_port}")
|
|
729
|
+
print(f"Robot Camera: {self.robot_camera_url or 'None'}")
|
|
730
|
+
print(f"Webcam: Device {self.webcam.device_id}")
|
|
731
|
+
|
|
732
|
+
if not self.connect():
|
|
733
|
+
print("Failed to connect!")
|
|
734
|
+
return
|
|
735
|
+
|
|
736
|
+
try:
|
|
737
|
+
self._main_menu()
|
|
738
|
+
finally:
|
|
739
|
+
self.disconnect()
|
|
740
|
+
|
|
741
|
+
def _main_menu(self):
|
|
742
|
+
"""Main interactive menu."""
|
|
743
|
+
while True:
|
|
744
|
+
print("\n" + "-" * 40)
|
|
745
|
+
print("Main Menu:")
|
|
746
|
+
print(" 1. Discover & Label Servos")
|
|
747
|
+
print(" 2. Define Joint Groups")
|
|
748
|
+
print(" 3. Record Poses")
|
|
749
|
+
print(" 4. Create Actions")
|
|
750
|
+
print(" 5. Generate Skills")
|
|
751
|
+
print(" 6. Execute Action")
|
|
752
|
+
print(" 7. Save Library")
|
|
753
|
+
print(" 8. Quick Servo Test")
|
|
754
|
+
print(" q. Quit")
|
|
755
|
+
print("-" * 40)
|
|
756
|
+
|
|
757
|
+
choice = input("Choice: ").strip().lower()
|
|
758
|
+
|
|
759
|
+
if choice == '1':
|
|
760
|
+
self._servo_discovery_flow()
|
|
761
|
+
elif choice == '2':
|
|
762
|
+
self._joint_groups_flow()
|
|
763
|
+
elif choice == '3':
|
|
764
|
+
self._pose_recording_flow()
|
|
765
|
+
elif choice == '4':
|
|
766
|
+
self._action_creation_flow()
|
|
767
|
+
elif choice == '5':
|
|
768
|
+
self._skill_generation_flow()
|
|
769
|
+
elif choice == '6':
|
|
770
|
+
self._execute_action_flow()
|
|
771
|
+
elif choice == '7':
|
|
772
|
+
self._save_library()
|
|
773
|
+
elif choice == '8':
|
|
774
|
+
self._quick_servo_test()
|
|
775
|
+
elif choice in ('q', 'quit', 'exit'):
|
|
776
|
+
break
|
|
777
|
+
|
|
778
|
+
def _servo_discovery_flow(self):
|
|
779
|
+
"""Discover and label all servos."""
|
|
780
|
+
print("\n=== Servo Discovery ===")
|
|
781
|
+
|
|
782
|
+
# Auto-discover
|
|
783
|
+
active_servos = self.calibrator.discover_servos()
|
|
784
|
+
|
|
785
|
+
if not active_servos:
|
|
786
|
+
print("No servos found!")
|
|
787
|
+
return
|
|
788
|
+
|
|
789
|
+
print(f"\nFound {len(active_servos)} servos: {active_servos}")
|
|
790
|
+
print("\nLabel each servo by moving it and observing the effect.")
|
|
791
|
+
|
|
792
|
+
for servo_id in active_servos:
|
|
793
|
+
label = input(f"\nLabel servo {servo_id}? [y/n/skip all]: ").strip().lower()
|
|
794
|
+
if label == 'skip all':
|
|
795
|
+
break
|
|
796
|
+
if label != 'y':
|
|
797
|
+
continue
|
|
798
|
+
|
|
799
|
+
cal = self.label_servo_interactive(servo_id)
|
|
800
|
+
self.calibrator.calibration.servos[servo_id] = cal
|
|
801
|
+
self.library.servo_labels[servo_id] = cal.name
|
|
802
|
+
|
|
803
|
+
print(f"Servo {servo_id} labeled as '{cal.name}'")
|
|
804
|
+
|
|
805
|
+
def _joint_groups_flow(self):
|
|
806
|
+
"""Define joint groups (e.g., left_leg, right_arm)."""
|
|
807
|
+
print("\n=== Joint Groups ===")
|
|
808
|
+
print("Group servos by function (e.g., 'left_front_leg': [1, 2, 3])")
|
|
809
|
+
|
|
810
|
+
servos = list(self.calibrator.calibration.servos.keys())
|
|
811
|
+
if not servos:
|
|
812
|
+
print("No servos discovered yet. Run servo discovery first.")
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
print(f"Available servos: {servos}")
|
|
816
|
+
for sid, cal in self.calibrator.calibration.servos.items():
|
|
817
|
+
print(f" {sid}: {cal.name}")
|
|
818
|
+
|
|
819
|
+
while True:
|
|
820
|
+
group_name = input("\nGroup name (or 'done'): ").strip()
|
|
821
|
+
if group_name.lower() == 'done':
|
|
822
|
+
break
|
|
823
|
+
|
|
824
|
+
servo_ids = input("Servo IDs (comma-separated): ").strip()
|
|
825
|
+
try:
|
|
826
|
+
ids = [int(s.strip()) for s in servo_ids.split(",")]
|
|
827
|
+
self.library.joint_groups[group_name] = ids
|
|
828
|
+
print(f"Created group '{group_name}': {ids}")
|
|
829
|
+
except ValueError:
|
|
830
|
+
print("Invalid input. Use: 1, 2, 3")
|
|
831
|
+
|
|
832
|
+
def _pose_recording_flow(self):
|
|
833
|
+
"""Record multiple poses."""
|
|
834
|
+
print("\n=== Pose Recording ===")
|
|
835
|
+
print("Record named poses for the robot.")
|
|
836
|
+
print("Commands: record, list, delete, test, done")
|
|
837
|
+
|
|
838
|
+
while True:
|
|
839
|
+
cmd = input("\nPose> ").strip().lower()
|
|
840
|
+
|
|
841
|
+
if cmd == 'record' or cmd == 'r':
|
|
842
|
+
self.record_pose_interactive()
|
|
843
|
+
|
|
844
|
+
elif cmd == 'list' or cmd == 'l':
|
|
845
|
+
poses = self.calibrator.calibration.poses
|
|
846
|
+
if poses:
|
|
847
|
+
for name, pose in poses.items():
|
|
848
|
+
print(f" {name}: {len(pose.servo_positions)} servos")
|
|
849
|
+
else:
|
|
850
|
+
print("No poses recorded")
|
|
851
|
+
|
|
852
|
+
elif cmd.startswith('delete '):
|
|
853
|
+
name = cmd[7:].strip()
|
|
854
|
+
if name in self.calibrator.calibration.poses:
|
|
855
|
+
del self.calibrator.calibration.poses[name]
|
|
856
|
+
print(f"Deleted pose '{name}'")
|
|
857
|
+
else:
|
|
858
|
+
print(f"Pose not found: {name}")
|
|
859
|
+
|
|
860
|
+
elif cmd.startswith('test '):
|
|
861
|
+
name = cmd[5:].strip()
|
|
862
|
+
pose = self.calibrator.calibration.get_pose(name)
|
|
863
|
+
if pose:
|
|
864
|
+
print(f"Applying pose '{name}'...")
|
|
865
|
+
self.calibrator.apply_pose(pose)
|
|
866
|
+
else:
|
|
867
|
+
print(f"Pose not found: {name}")
|
|
868
|
+
|
|
869
|
+
elif cmd in ('done', 'q'):
|
|
870
|
+
break
|
|
871
|
+
|
|
872
|
+
def _action_creation_flow(self):
|
|
873
|
+
"""Create actions from poses."""
|
|
874
|
+
print("\n=== Action Creation ===")
|
|
875
|
+
print("Create multi-step actions from recorded poses.")
|
|
876
|
+
print("Commands: create, list, preview, delete, done")
|
|
877
|
+
|
|
878
|
+
while True:
|
|
879
|
+
cmd = input("\nAction> ").strip().lower()
|
|
880
|
+
|
|
881
|
+
if cmd == 'create' or cmd == 'c':
|
|
882
|
+
self.create_action_interactive()
|
|
883
|
+
|
|
884
|
+
elif cmd == 'list' or cmd == 'l':
|
|
885
|
+
if self.library.actions:
|
|
886
|
+
for name, action in self.library.actions.items():
|
|
887
|
+
print(f" {name}: {len(action.steps)} steps ({action.action_type.value})")
|
|
888
|
+
else:
|
|
889
|
+
print("No actions created")
|
|
890
|
+
|
|
891
|
+
elif cmd.startswith('preview '):
|
|
892
|
+
name = cmd[8:].strip()
|
|
893
|
+
action = self.library.actions.get(name)
|
|
894
|
+
if action:
|
|
895
|
+
self.execute_action(action)
|
|
896
|
+
else:
|
|
897
|
+
print(f"Action not found: {name}")
|
|
898
|
+
|
|
899
|
+
elif cmd.startswith('delete '):
|
|
900
|
+
name = cmd[7:].strip()
|
|
901
|
+
if name in self.library.actions:
|
|
902
|
+
del self.library.actions[name]
|
|
903
|
+
print(f"Deleted action '{name}'")
|
|
904
|
+
else:
|
|
905
|
+
print(f"Action not found: {name}")
|
|
906
|
+
|
|
907
|
+
elif cmd in ('done', 'q'):
|
|
908
|
+
break
|
|
909
|
+
|
|
910
|
+
def _skill_generation_flow(self):
|
|
911
|
+
"""Generate Python skill code."""
|
|
912
|
+
print("\n=== Skill Generation ===")
|
|
913
|
+
|
|
914
|
+
if not self.library.actions:
|
|
915
|
+
print("No actions to generate skills from.")
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
print("Available actions:")
|
|
919
|
+
for name in self.library.actions:
|
|
920
|
+
print(f" {name}")
|
|
921
|
+
|
|
922
|
+
action_name = input("\nAction to generate (or 'all'): ").strip()
|
|
923
|
+
|
|
924
|
+
skills_dir = Path.home() / ".ate" / "skills" / self.robot_name
|
|
925
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
926
|
+
|
|
927
|
+
if action_name == 'all':
|
|
928
|
+
for name, action in self.library.actions.items():
|
|
929
|
+
code = self.generate_skill_code(action)
|
|
930
|
+
path = skills_dir / f"{name}.py"
|
|
931
|
+
with open(path, 'w') as f:
|
|
932
|
+
f.write(code)
|
|
933
|
+
print(f"Generated: {path}")
|
|
934
|
+
else:
|
|
935
|
+
action = self.library.actions.get(action_name)
|
|
936
|
+
if action:
|
|
937
|
+
code = self.generate_skill_code(action)
|
|
938
|
+
path = skills_dir / f"{action_name}.py"
|
|
939
|
+
with open(path, 'w') as f:
|
|
940
|
+
f.write(code)
|
|
941
|
+
print(f"Generated: {path}")
|
|
942
|
+
print("\n--- Generated Code ---")
|
|
943
|
+
print(code)
|
|
944
|
+
else:
|
|
945
|
+
print(f"Action not found: {action_name}")
|
|
946
|
+
|
|
947
|
+
def _execute_action_flow(self):
|
|
948
|
+
"""Execute a saved action."""
|
|
949
|
+
if not self.library.actions:
|
|
950
|
+
print("No actions available.")
|
|
951
|
+
return
|
|
952
|
+
|
|
953
|
+
print("Available actions:")
|
|
954
|
+
for name in self.library.actions:
|
|
955
|
+
print(f" {name}")
|
|
956
|
+
|
|
957
|
+
name = input("Action to execute: ").strip()
|
|
958
|
+
action = self.library.actions.get(name)
|
|
959
|
+
|
|
960
|
+
if action:
|
|
961
|
+
self.execute_action(action)
|
|
962
|
+
else:
|
|
963
|
+
print(f"Action not found: {name}")
|
|
964
|
+
|
|
965
|
+
def _save_library(self):
|
|
966
|
+
"""Save skill library and calibration."""
|
|
967
|
+
# Save calibration
|
|
968
|
+
save_calibration(self.calibrator.calibration)
|
|
969
|
+
|
|
970
|
+
# Save skill library
|
|
971
|
+
lib_path = Path.home() / ".ate" / "skill_libraries" / f"{self.robot_name}.json"
|
|
972
|
+
self.library.calibration = self.calibrator.calibration
|
|
973
|
+
self.library.save(lib_path)
|
|
974
|
+
|
|
975
|
+
print(f"Saved calibration: ~/.ate/calibrations/{self.robot_name}.json")
|
|
976
|
+
print(f"Saved skill library: {lib_path}")
|
|
977
|
+
|
|
978
|
+
def _quick_servo_test(self):
|
|
979
|
+
"""Quick servo test without full labeling."""
|
|
980
|
+
print("\n=== Quick Servo Test ===")
|
|
981
|
+
print("Commands: <servo_id> <value> (e.g., '11 2500'), or 'done'")
|
|
982
|
+
|
|
983
|
+
while True:
|
|
984
|
+
cmd = input("Test> ").strip()
|
|
985
|
+
if cmd.lower() in ('done', 'q'):
|
|
986
|
+
break
|
|
987
|
+
|
|
988
|
+
try:
|
|
989
|
+
parts = cmd.split()
|
|
990
|
+
servo_id = int(parts[0])
|
|
991
|
+
value = int(parts[1]) if len(parts) > 1 else 1500
|
|
992
|
+
|
|
993
|
+
print(f"Moving servo {servo_id} to {value}...")
|
|
994
|
+
self.calibrator.set_servo(servo_id, value, 500)
|
|
995
|
+
self.webcam.show_preview("Servo Test")
|
|
996
|
+
|
|
997
|
+
except (ValueError, IndexError):
|
|
998
|
+
print("Format: <servo_id> <value> (e.g., '11 2500')")
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
# =============================================================================
|
|
1002
|
+
# CLI Integration
|
|
1003
|
+
# =============================================================================
|
|
1004
|
+
|
|
1005
|
+
def visual_label_command(
|
|
1006
|
+
port: str,
|
|
1007
|
+
name: str = "robot",
|
|
1008
|
+
robot_type: str = "unknown",
|
|
1009
|
+
webcam_id: int = 0,
|
|
1010
|
+
camera_url: Optional[str] = None,
|
|
1011
|
+
):
|
|
1012
|
+
"""Run the visual labeling session."""
|
|
1013
|
+
labeler = DualCameraLabeler(
|
|
1014
|
+
serial_port=port,
|
|
1015
|
+
robot_name=name,
|
|
1016
|
+
robot_model=robot_type,
|
|
1017
|
+
webcam_id=webcam_id,
|
|
1018
|
+
robot_camera_url=camera_url,
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
labeler.run_interactive_session()
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def load_skill_library(robot_name: str) -> Optional[SkillLibrary]:
|
|
1025
|
+
"""Load a saved skill library."""
|
|
1026
|
+
lib_path = Path.home() / ".ate" / "skill_libraries" / f"{robot_name}.json"
|
|
1027
|
+
cal = load_calibration(robot_name)
|
|
1028
|
+
|
|
1029
|
+
if lib_path.exists():
|
|
1030
|
+
return SkillLibrary.load(lib_path, cal)
|
|
1031
|
+
return None
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def list_skill_libraries() -> List[str]:
|
|
1035
|
+
"""List available skill libraries."""
|
|
1036
|
+
lib_dir = Path.home() / ".ate" / "skill_libraries"
|
|
1037
|
+
if not lib_dir.exists():
|
|
1038
|
+
return []
|
|
1039
|
+
return [p.stem for p in lib_dir.glob("*.json")]
|