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,524 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recording commands for FoodforThought CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- ate record start - Start recording telemetry
|
|
6
|
+
- ate record stop - Stop recording and upload
|
|
7
|
+
- ate record status - Get current recording status
|
|
8
|
+
- ate record demo - Record a timed demonstration
|
|
9
|
+
- ate record list - List telemetry recordings
|
|
10
|
+
- ate record local - Record from a connected robot
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional, List
|
|
18
|
+
|
|
19
|
+
CONFIG_DIR = Path.home() / ".ate"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def record_start(client, robot_id: str, skill_id: str, task_description: Optional[str] = None) -> None:
|
|
23
|
+
"""Start recording telemetry from a robot."""
|
|
24
|
+
import uuid
|
|
25
|
+
|
|
26
|
+
# Store recording state in a file
|
|
27
|
+
recording_file = CONFIG_DIR / "active_recording.json"
|
|
28
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
29
|
+
|
|
30
|
+
if recording_file.exists():
|
|
31
|
+
print("Error: Recording already in progress. Run 'ate record stop' first.", file=sys.stderr)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
recording_id = str(uuid.uuid4())
|
|
35
|
+
recording_state = {
|
|
36
|
+
"id": recording_id,
|
|
37
|
+
"robot_id": robot_id,
|
|
38
|
+
"skill_id": skill_id,
|
|
39
|
+
"task_description": task_description or "",
|
|
40
|
+
"start_time": time.time(),
|
|
41
|
+
"frames": [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
with open(recording_file, "w") as f:
|
|
45
|
+
json.dump(recording_state, f, indent=2)
|
|
46
|
+
|
|
47
|
+
print(f"Recording started!")
|
|
48
|
+
print(f" Recording ID: {recording_id}")
|
|
49
|
+
print(f" Robot: {robot_id}")
|
|
50
|
+
print(f" Skill: {skill_id}")
|
|
51
|
+
if task_description:
|
|
52
|
+
print(f" Task: {task_description}")
|
|
53
|
+
print(f"\nRun 'ate record stop' when finished.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def record_stop(client, success: bool = True, notes: Optional[str] = None,
|
|
57
|
+
upload: bool = True, create_labeling_task: bool = False) -> None:
|
|
58
|
+
"""Stop recording and optionally upload to FoodforThought."""
|
|
59
|
+
from datetime import datetime
|
|
60
|
+
|
|
61
|
+
recording_file = CONFIG_DIR / "active_recording.json"
|
|
62
|
+
|
|
63
|
+
if not recording_file.exists():
|
|
64
|
+
print("Error: No active recording. Start one with 'ate record start'.", file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
with open(recording_file, "r") as f:
|
|
68
|
+
recording_state = json.load(f)
|
|
69
|
+
|
|
70
|
+
# Calculate duration
|
|
71
|
+
end_time = time.time()
|
|
72
|
+
duration = end_time - recording_state["start_time"]
|
|
73
|
+
frame_count = len(recording_state.get("frames", []))
|
|
74
|
+
|
|
75
|
+
print(f"Recording stopped!")
|
|
76
|
+
print(f" Recording ID: {recording_state['id']}")
|
|
77
|
+
print(f" Duration: {duration:.1f}s")
|
|
78
|
+
print(f" Frames: {frame_count}")
|
|
79
|
+
print(f" Success: {'Yes' if success else 'No'}")
|
|
80
|
+
|
|
81
|
+
if upload:
|
|
82
|
+
print(f"\nUploading to FoodforThought...")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
recording_data = {
|
|
86
|
+
"recording": {
|
|
87
|
+
"id": recording_state["id"],
|
|
88
|
+
"robotId": recording_state["robot_id"],
|
|
89
|
+
"skillId": recording_state["skill_id"],
|
|
90
|
+
"source": "hardware",
|
|
91
|
+
"startTime": datetime.fromtimestamp(recording_state["start_time"]).isoformat(),
|
|
92
|
+
"endTime": datetime.fromtimestamp(end_time).isoformat(),
|
|
93
|
+
"success": success,
|
|
94
|
+
"metadata": {
|
|
95
|
+
"duration": duration,
|
|
96
|
+
"frameRate": frame_count / duration if duration > 0 else 0,
|
|
97
|
+
"totalFrames": frame_count,
|
|
98
|
+
"tags": ["edge_recording", "cli"],
|
|
99
|
+
"notes": notes,
|
|
100
|
+
},
|
|
101
|
+
"frames": recording_state.get("frames", []),
|
|
102
|
+
"events": [],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if create_labeling_task:
|
|
107
|
+
recording_data["createLabelingTask"] = True
|
|
108
|
+
|
|
109
|
+
response = client._request("POST", "/telemetry/ingest", json=recording_data)
|
|
110
|
+
|
|
111
|
+
artifact_id = response.get("data", {}).get("artifactId", "")
|
|
112
|
+
print(f"\n✓ Uploaded successfully!")
|
|
113
|
+
print(f" Artifact ID: {artifact_id}")
|
|
114
|
+
print(f" View at: https://foodforthought.kindly.fyi/artifacts/{artifact_id}")
|
|
115
|
+
|
|
116
|
+
if create_labeling_task:
|
|
117
|
+
task_id = response.get("data", {}).get("taskId", "")
|
|
118
|
+
if task_id:
|
|
119
|
+
print(f" Labeling Task: https://foodforthought.kindly.fyi/labeling/{task_id}")
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"\n✗ Upload failed: {e}", file=sys.stderr)
|
|
123
|
+
print("Recording saved locally. You can upload later.", file=sys.stderr)
|
|
124
|
+
|
|
125
|
+
if notes:
|
|
126
|
+
print(f"\nNotes: {notes}")
|
|
127
|
+
|
|
128
|
+
# Remove recording state file
|
|
129
|
+
recording_file.unlink()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def record_status(client) -> None:
|
|
133
|
+
"""Get current recording status."""
|
|
134
|
+
recording_file = CONFIG_DIR / "active_recording.json"
|
|
135
|
+
|
|
136
|
+
if not recording_file.exists():
|
|
137
|
+
print("No active recording session.")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
with open(recording_file, "r") as f:
|
|
141
|
+
recording_state = json.load(f)
|
|
142
|
+
|
|
143
|
+
elapsed = time.time() - recording_state["start_time"]
|
|
144
|
+
frame_count = len(recording_state.get("frames", []))
|
|
145
|
+
|
|
146
|
+
print(f"Recording in progress")
|
|
147
|
+
print(f" Recording ID: {recording_state['id']}")
|
|
148
|
+
print(f" Robot: {recording_state['robot_id']}")
|
|
149
|
+
print(f" Skill: {recording_state['skill_id']}")
|
|
150
|
+
print(f" Elapsed: {elapsed:.1f}s")
|
|
151
|
+
print(f" Frames: {frame_count}")
|
|
152
|
+
if recording_state.get("task_description"):
|
|
153
|
+
print(f" Task: {recording_state['task_description']}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def record_demo(client, robot_id: str, skill_id: str, task_description: str,
|
|
157
|
+
duration_seconds: float = 30.0, create_labeling_task: bool = True) -> None:
|
|
158
|
+
"""Record a timed demonstration."""
|
|
159
|
+
import uuid
|
|
160
|
+
from datetime import datetime
|
|
161
|
+
|
|
162
|
+
recording_id = str(uuid.uuid4())
|
|
163
|
+
print(f"Recording demonstration...")
|
|
164
|
+
print(f" Recording ID: {recording_id}")
|
|
165
|
+
print(f" Robot: {robot_id}")
|
|
166
|
+
print(f" Skill: {skill_id}")
|
|
167
|
+
print(f" Task: {task_description}")
|
|
168
|
+
print(f" Duration: {duration_seconds}s")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
start_time = time.time()
|
|
172
|
+
|
|
173
|
+
# Show a countdown/progress indicator
|
|
174
|
+
elapsed = 0
|
|
175
|
+
while elapsed < duration_seconds:
|
|
176
|
+
remaining = duration_seconds - elapsed
|
|
177
|
+
print(f"\rRecording... {remaining:.0f}s remaining", end="", flush=True)
|
|
178
|
+
time.sleep(min(1.0, remaining))
|
|
179
|
+
elapsed = time.time() - start_time
|
|
180
|
+
|
|
181
|
+
end_time = time.time()
|
|
182
|
+
actual_duration = end_time - start_time
|
|
183
|
+
print(f"\rRecording complete!{' ' * 20}")
|
|
184
|
+
|
|
185
|
+
print(f"\nUploading to FoodforThought...")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
recording_data = {
|
|
189
|
+
"recording": {
|
|
190
|
+
"id": recording_id,
|
|
191
|
+
"robotId": robot_id,
|
|
192
|
+
"skillId": skill_id,
|
|
193
|
+
"source": "hardware",
|
|
194
|
+
"startTime": datetime.fromtimestamp(start_time).isoformat(),
|
|
195
|
+
"endTime": datetime.fromtimestamp(end_time).isoformat(),
|
|
196
|
+
"success": True,
|
|
197
|
+
"metadata": {
|
|
198
|
+
"duration": actual_duration,
|
|
199
|
+
"frameRate": 0,
|
|
200
|
+
"totalFrames": 0,
|
|
201
|
+
"tags": ["demonstration", "cli"],
|
|
202
|
+
"task_description": task_description,
|
|
203
|
+
},
|
|
204
|
+
"frames": [],
|
|
205
|
+
"events": [],
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if create_labeling_task:
|
|
210
|
+
recording_data["createLabelingTask"] = True
|
|
211
|
+
|
|
212
|
+
response = client._request("POST", "/telemetry/ingest", json=recording_data)
|
|
213
|
+
|
|
214
|
+
artifact_id = response.get("data", {}).get("artifactId", "")
|
|
215
|
+
print(f"\n✓ Uploaded successfully!")
|
|
216
|
+
print(f" Artifact ID: {artifact_id}")
|
|
217
|
+
print(f" View at: https://foodforthought.kindly.fyi/artifacts/{artifact_id}")
|
|
218
|
+
|
|
219
|
+
if create_labeling_task:
|
|
220
|
+
task_id = response.get("data", {}).get("taskId", "")
|
|
221
|
+
if task_id:
|
|
222
|
+
print(f" Labeling Task: https://foodforthought.kindly.fyi/labeling/{task_id}")
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
print(f"\n✗ Upload failed: {e}", file=sys.stderr)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def record_list(client, robot_id: Optional[str] = None, skill_id: Optional[str] = None,
|
|
229
|
+
success_only: bool = False, limit: int = 20) -> None:
|
|
230
|
+
"""List telemetry recordings from FoodforThought."""
|
|
231
|
+
print("Fetching recordings...")
|
|
232
|
+
|
|
233
|
+
params = {
|
|
234
|
+
"type": "trajectory",
|
|
235
|
+
"limit": limit,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if robot_id:
|
|
239
|
+
params["robotModel"] = robot_id
|
|
240
|
+
if skill_id:
|
|
241
|
+
params["task"] = skill_id
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
response = client._request("GET", "/artifacts", params=params)
|
|
245
|
+
artifacts = response.get("artifacts", [])
|
|
246
|
+
|
|
247
|
+
if not artifacts:
|
|
248
|
+
print("No recordings found.")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
print(f"\nFound {len(artifacts)} recording(s):\n")
|
|
252
|
+
|
|
253
|
+
for artifact in artifacts:
|
|
254
|
+
metadata = artifact.get("metadata", {})
|
|
255
|
+
|
|
256
|
+
# Skip failed recordings if success_only
|
|
257
|
+
if success_only and not metadata.get("success", True):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
success_marker = "✓" if metadata.get("success", True) else "✗"
|
|
261
|
+
print(f"{success_marker} {artifact.get('name', 'Unnamed')}")
|
|
262
|
+
print(f" ID: {artifact.get('id')}")
|
|
263
|
+
print(f" Robot: {metadata.get('robotId', 'Unknown')}")
|
|
264
|
+
print(f" Skill: {metadata.get('skillId', 'Unknown')}")
|
|
265
|
+
print(f" Duration: {metadata.get('duration', 0):.1f}s")
|
|
266
|
+
print(f" Frames: {metadata.get('frameCount', 0)}")
|
|
267
|
+
print(f" Source: {metadata.get('source', 'Unknown')}")
|
|
268
|
+
print()
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
print(f"Error fetching recordings: {e}", file=sys.stderr)
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def record_local(
|
|
276
|
+
client,
|
|
277
|
+
profile_name: str,
|
|
278
|
+
duration: float = 30.0,
|
|
279
|
+
name: Optional[str] = None,
|
|
280
|
+
fps: float = 5.0,
|
|
281
|
+
detect_trash: bool = False,
|
|
282
|
+
detect_colors: Optional[List[str]] = None,
|
|
283
|
+
output_path: Optional[str] = None,
|
|
284
|
+
upload: bool = True,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Record from a connected robot with visual capture and detection."""
|
|
287
|
+
from ate.robot.profiles import load_profile, list_profiles
|
|
288
|
+
from ate.robot.manager import RobotManager
|
|
289
|
+
from ate.recording import VisualRecordingSession
|
|
290
|
+
|
|
291
|
+
# Load profile
|
|
292
|
+
profile = load_profile(profile_name)
|
|
293
|
+
if not profile:
|
|
294
|
+
available = list_profiles()
|
|
295
|
+
print(f"Profile '{profile_name}' not found.", file=sys.stderr)
|
|
296
|
+
if available:
|
|
297
|
+
print(f"Available profiles: {', '.join(p.name for p in available)}")
|
|
298
|
+
else:
|
|
299
|
+
print("Run 'ate robot setup' to create a profile first.")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
recording_name = name or f"{profile.name}_recording"
|
|
303
|
+
output = output_path or f"{recording_name}.demonstration"
|
|
304
|
+
|
|
305
|
+
print(f"Local Recording")
|
|
306
|
+
print(f"{'=' * 40}")
|
|
307
|
+
print(f"Profile: {profile.name}")
|
|
308
|
+
print(f"Robot type: {profile.robot_type}")
|
|
309
|
+
print(f"Duration: {duration}s")
|
|
310
|
+
print(f"Visual capture: {fps} fps")
|
|
311
|
+
print()
|
|
312
|
+
|
|
313
|
+
# Connect to robot
|
|
314
|
+
print("Connecting to robot...")
|
|
315
|
+
try:
|
|
316
|
+
manager = RobotManager()
|
|
317
|
+
managed = manager.load(profile_name)
|
|
318
|
+
if not managed:
|
|
319
|
+
print(f"Failed to load profile: {profile_name}", file=sys.stderr)
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
if not manager.connect(profile_name):
|
|
322
|
+
print(f"Failed to connect to robot", file=sys.stderr)
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
driver = manager.get(profile_name)
|
|
325
|
+
print(f" Connected to {driver.get_info().name}")
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"Failed to connect: {e}", file=sys.stderr)
|
|
328
|
+
sys.exit(1)
|
|
329
|
+
|
|
330
|
+
# Setup detector
|
|
331
|
+
detector = None
|
|
332
|
+
if detect_trash:
|
|
333
|
+
try:
|
|
334
|
+
from ate.detection import TrashDetector
|
|
335
|
+
detector = TrashDetector()
|
|
336
|
+
print(f" Trash detection enabled")
|
|
337
|
+
except ImportError:
|
|
338
|
+
print(" Warning: Trash detection requires Pillow. Run: pip install 'foodforthought-cli[detection]'")
|
|
339
|
+
elif detect_colors:
|
|
340
|
+
try:
|
|
341
|
+
from ate.detection import ColorDetector
|
|
342
|
+
detector = ColorDetector()
|
|
343
|
+
print(f" Color detection enabled: {', '.join(detect_colors)}")
|
|
344
|
+
except ImportError:
|
|
345
|
+
print(" Warning: Color detection requires Pillow. Run: pip install 'foodforthought-cli[detection]'")
|
|
346
|
+
|
|
347
|
+
# Check if camera is available
|
|
348
|
+
has_camera = hasattr(driver, 'get_image')
|
|
349
|
+
if has_camera:
|
|
350
|
+
print(f" Camera available")
|
|
351
|
+
else:
|
|
352
|
+
print(f" No camera available (recording actions only)")
|
|
353
|
+
|
|
354
|
+
print()
|
|
355
|
+
print("Starting recording in 3 seconds...")
|
|
356
|
+
print("(Control the robot manually during recording)")
|
|
357
|
+
time.sleep(3)
|
|
358
|
+
|
|
359
|
+
# Record
|
|
360
|
+
print()
|
|
361
|
+
print("RECORDING - Press Ctrl+C to stop early")
|
|
362
|
+
print("-" * 40)
|
|
363
|
+
|
|
364
|
+
session = None
|
|
365
|
+
try:
|
|
366
|
+
with VisualRecordingSession(
|
|
367
|
+
driver,
|
|
368
|
+
name=recording_name,
|
|
369
|
+
capture_fps=fps,
|
|
370
|
+
detector=detector,
|
|
371
|
+
) as session:
|
|
372
|
+
start_time = time.time()
|
|
373
|
+
elapsed = 0
|
|
374
|
+
|
|
375
|
+
while elapsed < duration:
|
|
376
|
+
remaining = duration - elapsed
|
|
377
|
+
frame_info = f", {session.frame_count} frames" if has_camera else ""
|
|
378
|
+
det_info = f", {session.detection_count} detections" if detector else ""
|
|
379
|
+
print(f"\rRecording... {remaining:.0f}s remaining{frame_info}{det_info} ", end="", flush=True)
|
|
380
|
+
time.sleep(0.5)
|
|
381
|
+
elapsed = time.time() - start_time
|
|
382
|
+
|
|
383
|
+
except KeyboardInterrupt:
|
|
384
|
+
print("\n\nRecording stopped by user")
|
|
385
|
+
|
|
386
|
+
if session is None:
|
|
387
|
+
print("No recording session created.", file=sys.stderr)
|
|
388
|
+
manager.disconnect_all()
|
|
389
|
+
sys.exit(1)
|
|
390
|
+
|
|
391
|
+
print("\n")
|
|
392
|
+
print("Recording complete!")
|
|
393
|
+
print("-" * 40)
|
|
394
|
+
print(session.summary())
|
|
395
|
+
|
|
396
|
+
# Save locally
|
|
397
|
+
print()
|
|
398
|
+
print(f"Saving to {output}...")
|
|
399
|
+
session.save(output)
|
|
400
|
+
|
|
401
|
+
if has_camera and session.frame_count > 0:
|
|
402
|
+
frames_path = output.replace(".demonstration", "_frames.json")
|
|
403
|
+
session.save_frames(frames_path, include_data=True)
|
|
404
|
+
print(f"Saved {session.frame_count} frames to {frames_path}")
|
|
405
|
+
|
|
406
|
+
# Upload if requested
|
|
407
|
+
if upload:
|
|
408
|
+
print()
|
|
409
|
+
print("Uploading to FoodforThought...")
|
|
410
|
+
try:
|
|
411
|
+
recording_data = session.to_dict()
|
|
412
|
+
response = client._request("POST", "/telemetry/ingest", json={"recording": recording_data})
|
|
413
|
+
artifact_id = response.get("data", {}).get("artifactId", "")
|
|
414
|
+
print(f"Uploaded successfully!")
|
|
415
|
+
print(f" Artifact ID: {artifact_id}")
|
|
416
|
+
print(f" View at: https://foodforthought.kindly.fyi/artifacts/{artifact_id}")
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print(f"Upload failed: {e}")
|
|
419
|
+
print("Recording saved locally - you can upload later with 'ate upload'")
|
|
420
|
+
|
|
421
|
+
# Disconnect
|
|
422
|
+
manager.disconnect_all()
|
|
423
|
+
print()
|
|
424
|
+
print("Done!")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def register_parser(subparsers):
|
|
428
|
+
"""Register recording commands with argparse."""
|
|
429
|
+
record_parser = subparsers.add_parser("record",
|
|
430
|
+
help="Record robot telemetry and demonstrations",
|
|
431
|
+
description="""Record robot telemetry for the data flywheel.
|
|
432
|
+
|
|
433
|
+
EXAMPLES:
|
|
434
|
+
ate record start my-robot-id my-skill-id
|
|
435
|
+
ate record stop --success --notes "Good demo"
|
|
436
|
+
ate record demo my-robot-id pick-object "Pick up the red ball" --duration 60
|
|
437
|
+
ate record local my_robot_profile --duration 30 --detect-trash
|
|
438
|
+
""")
|
|
439
|
+
record_subparsers = record_parser.add_subparsers(dest="record_action")
|
|
440
|
+
|
|
441
|
+
# record start
|
|
442
|
+
record_start_parser = record_subparsers.add_parser("start", help="Start recording telemetry")
|
|
443
|
+
record_start_parser.add_argument("robot", help="Robot ID")
|
|
444
|
+
record_start_parser.add_argument("skill", help="Skill ID being demonstrated")
|
|
445
|
+
record_start_parser.add_argument("-t", "--task", help="Task description")
|
|
446
|
+
|
|
447
|
+
# record stop
|
|
448
|
+
record_stop_parser = record_subparsers.add_parser("stop", help="Stop recording and upload")
|
|
449
|
+
record_stop_parser.add_argument("--success", dest="success", action="store_true",
|
|
450
|
+
default=True, help="Mark recording as successful (default)")
|
|
451
|
+
record_stop_parser.add_argument("--failure", dest="success", action="store_false",
|
|
452
|
+
help="Mark recording as failed")
|
|
453
|
+
record_stop_parser.add_argument("-n", "--notes", help="Recording notes")
|
|
454
|
+
record_stop_parser.add_argument("--no-upload", action="store_true",
|
|
455
|
+
help="Skip uploading to FoodforThought")
|
|
456
|
+
record_stop_parser.add_argument("--create-task", action="store_true",
|
|
457
|
+
help="Create labeling task from this recording")
|
|
458
|
+
|
|
459
|
+
# record status
|
|
460
|
+
record_subparsers.add_parser("status", help="Get current recording status")
|
|
461
|
+
|
|
462
|
+
# record demo
|
|
463
|
+
record_demo_parser = record_subparsers.add_parser("demo", help="Record a timed demonstration")
|
|
464
|
+
record_demo_parser.add_argument("robot", help="Robot ID")
|
|
465
|
+
record_demo_parser.add_argument("skill", help="Skill ID")
|
|
466
|
+
record_demo_parser.add_argument("task", help="Task description")
|
|
467
|
+
record_demo_parser.add_argument("-d", "--duration", type=float, default=30.0,
|
|
468
|
+
help="Recording duration in seconds")
|
|
469
|
+
record_demo_parser.add_argument("--create-task", dest="create_task", action="store_true",
|
|
470
|
+
default=True, help="Create labeling task (default)")
|
|
471
|
+
|
|
472
|
+
# record list
|
|
473
|
+
record_list_parser = record_subparsers.add_parser("list", help="List telemetry recordings")
|
|
474
|
+
record_list_parser.add_argument("-r", "--robot", help="Filter by robot")
|
|
475
|
+
record_list_parser.add_argument("-s", "--skill", help="Filter by skill")
|
|
476
|
+
record_list_parser.add_argument("--success-only", action="store_true",
|
|
477
|
+
help="Only show successful recordings")
|
|
478
|
+
record_list_parser.add_argument("-l", "--limit", type=int, default=20,
|
|
479
|
+
help="Max number of recordings to show")
|
|
480
|
+
|
|
481
|
+
# record local
|
|
482
|
+
record_local_parser = record_subparsers.add_parser("local",
|
|
483
|
+
help="Record from a connected robot with visual capture")
|
|
484
|
+
record_local_parser.add_argument("profile", help="Robot profile name")
|
|
485
|
+
record_local_parser.add_argument("-d", "--duration", type=float, default=30.0,
|
|
486
|
+
help="Recording duration in seconds")
|
|
487
|
+
record_local_parser.add_argument("-n", "--name", help="Recording name")
|
|
488
|
+
record_local_parser.add_argument("--fps", type=float, default=5.0,
|
|
489
|
+
help="Frame capture rate")
|
|
490
|
+
record_local_parser.add_argument("--detect-trash", action="store_true",
|
|
491
|
+
help="Enable trash detection during recording")
|
|
492
|
+
record_local_parser.add_argument("--detect-colors", nargs="+",
|
|
493
|
+
help="Enable color detection (specify colors)")
|
|
494
|
+
record_local_parser.add_argument("-o", "--output", help="Output file path")
|
|
495
|
+
record_local_parser.add_argument("--no-upload", action="store_true",
|
|
496
|
+
help="Skip uploading to FoodforThought")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def handle(client, args):
|
|
500
|
+
"""Handle recording commands."""
|
|
501
|
+
if not hasattr(args, 'record_action') or not args.record_action:
|
|
502
|
+
print("Usage: ate record <start|stop|status|demo|list|local>")
|
|
503
|
+
sys.exit(1)
|
|
504
|
+
|
|
505
|
+
if args.record_action == "start":
|
|
506
|
+
record_start(client, args.robot, args.skill, getattr(args, 'task', None))
|
|
507
|
+
elif args.record_action == "stop":
|
|
508
|
+
success = args.success
|
|
509
|
+
upload_flag = not getattr(args, 'no_upload', False)
|
|
510
|
+
record_stop(client, success, getattr(args, 'notes', None),
|
|
511
|
+
upload_flag, getattr(args, 'create_task', False))
|
|
512
|
+
elif args.record_action == "status":
|
|
513
|
+
record_status(client)
|
|
514
|
+
elif args.record_action == "demo":
|
|
515
|
+
record_demo(client, args.robot, args.skill, args.task,
|
|
516
|
+
args.duration, getattr(args, 'create_task', True))
|
|
517
|
+
elif args.record_action == "list":
|
|
518
|
+
record_list(client, getattr(args, 'robot', None), getattr(args, 'skill', None),
|
|
519
|
+
getattr(args, 'success_only', False), getattr(args, 'limit', 20))
|
|
520
|
+
elif args.record_action == "local":
|
|
521
|
+
record_local(client, args.profile, args.duration, getattr(args, 'name', None),
|
|
522
|
+
args.fps, getattr(args, 'detect_trash', False),
|
|
523
|
+
getattr(args, 'detect_colors', None), getattr(args, 'output', None),
|
|
524
|
+
not getattr(args, 'no_upload', False))
|
ate/commands/repo.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository commands for FoodforThought CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- ate init - Initialize a new repository
|
|
6
|
+
- ate clone - Clone a repository
|
|
7
|
+
- ate commit - Create a commit
|
|
8
|
+
- ate push - Push commits to remote
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, List, Dict
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def init_repo(client, name: str, description: str = "", visibility: str = "public") -> Dict:
|
|
20
|
+
"""Initialize a new repository."""
|
|
21
|
+
data = {
|
|
22
|
+
"name": name,
|
|
23
|
+
"description": description,
|
|
24
|
+
"visibility": visibility,
|
|
25
|
+
"robotModels": [],
|
|
26
|
+
"taskDomain": None,
|
|
27
|
+
}
|
|
28
|
+
return client._request("POST", "/repositories", json=data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clone_repo(client, repo_id: str, target_dir: Optional[str] = None) -> None:
|
|
32
|
+
"""Clone a repository."""
|
|
33
|
+
repo = client._request("GET", f"/repositories/{repo_id}")
|
|
34
|
+
repo_data = repo["repository"]
|
|
35
|
+
|
|
36
|
+
if target_dir is None:
|
|
37
|
+
target_dir = repo_data["name"]
|
|
38
|
+
|
|
39
|
+
target_path = Path(target_dir)
|
|
40
|
+
target_path.mkdir(exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Create .ate directory
|
|
43
|
+
ate_dir = target_path / ".ate"
|
|
44
|
+
ate_dir.mkdir(exist_ok=True)
|
|
45
|
+
|
|
46
|
+
# Save repository metadata
|
|
47
|
+
metadata = {
|
|
48
|
+
"id": repo_data["id"],
|
|
49
|
+
"name": repo_data["name"],
|
|
50
|
+
"owner": repo_data["owner"]["email"],
|
|
51
|
+
"url": f"{client.base_url}/repositories/{repo_data['id']}",
|
|
52
|
+
}
|
|
53
|
+
with open(ate_dir / "config.json", "w") as f:
|
|
54
|
+
json.dump(metadata, f, indent=2)
|
|
55
|
+
|
|
56
|
+
# Download files
|
|
57
|
+
items = repo_data.get("items", [])
|
|
58
|
+
for item in items:
|
|
59
|
+
if item.get("fileStorage"):
|
|
60
|
+
file_url = item["fileStorage"]["url"]
|
|
61
|
+
file_path = target_path / item["filePath"]
|
|
62
|
+
|
|
63
|
+
# Create directory if needed
|
|
64
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Download file
|
|
67
|
+
file_response = requests.get(file_url)
|
|
68
|
+
file_response.raise_for_status()
|
|
69
|
+
with open(file_path, "wb") as f:
|
|
70
|
+
f.write(file_response.content)
|
|
71
|
+
|
|
72
|
+
print(f"Cloned repository '{repo_data['name']}' to '{target_dir}'")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def commit(client, message: str, files: Optional[List[str]] = None) -> Dict:
|
|
76
|
+
"""Create a commit."""
|
|
77
|
+
# Find .ate directory
|
|
78
|
+
ate_dir = Path(".ate")
|
|
79
|
+
if not ate_dir.exists():
|
|
80
|
+
print("Error: Not a FoodforThought repository. Run 'ate init' first.", file=sys.stderr)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
with open(ate_dir / "config.json") as f:
|
|
84
|
+
config = json.load(f)
|
|
85
|
+
|
|
86
|
+
repo_id = config["id"]
|
|
87
|
+
|
|
88
|
+
# Get current files if not specified
|
|
89
|
+
if files is None:
|
|
90
|
+
# This would need to track changes - simplified for now
|
|
91
|
+
files = []
|
|
92
|
+
|
|
93
|
+
# For now, return a placeholder
|
|
94
|
+
# In a full implementation, this would:
|
|
95
|
+
# 1. Track file changes
|
|
96
|
+
# 2. Upload new/modified files
|
|
97
|
+
# 3. Create commit via API
|
|
98
|
+
print(f"Creating commit: {message}")
|
|
99
|
+
print("Note: Full commit functionality requires file tracking implementation")
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def push(client, branch: str = "main") -> None:
|
|
104
|
+
"""Push commits to remote."""
|
|
105
|
+
ate_dir = Path(".ate")
|
|
106
|
+
if not ate_dir.exists():
|
|
107
|
+
print("Error: Not a FoodforThought repository.", file=sys.stderr)
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
with open(ate_dir / "config.json") as f:
|
|
111
|
+
config = json.load(f)
|
|
112
|
+
|
|
113
|
+
repo_id = config["id"]
|
|
114
|
+
print(f"Pushing to {branch} branch...")
|
|
115
|
+
print("Note: Full push functionality requires commit tracking implementation")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def register_parser(subparsers):
|
|
119
|
+
"""Register repo commands with argparse."""
|
|
120
|
+
# init command
|
|
121
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new repository")
|
|
122
|
+
init_parser.add_argument("name", help="Repository name")
|
|
123
|
+
init_parser.add_argument("-d", "--description", default="", help="Repository description")
|
|
124
|
+
init_parser.add_argument(
|
|
125
|
+
"-v", "--visibility", choices=["public", "private"], default="public",
|
|
126
|
+
help="Repository visibility"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# clone command
|
|
130
|
+
clone_parser = subparsers.add_parser("clone", help="Clone a repository")
|
|
131
|
+
clone_parser.add_argument("repo_id", help="Repository ID")
|
|
132
|
+
clone_parser.add_argument("target_dir", nargs="?", help="Target directory")
|
|
133
|
+
|
|
134
|
+
# commit command
|
|
135
|
+
commit_parser = subparsers.add_parser("commit", help="Create a commit")
|
|
136
|
+
commit_parser.add_argument("-m", "--message", required=True, help="Commit message")
|
|
137
|
+
commit_parser.add_argument("files", nargs="*", help="Files to commit")
|
|
138
|
+
|
|
139
|
+
# push command
|
|
140
|
+
push_parser = subparsers.add_parser("push", help="Push commits to remote")
|
|
141
|
+
push_parser.add_argument("-b", "--branch", default="main", help="Branch name")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def handle(client, args):
|
|
145
|
+
"""Handle repo commands."""
|
|
146
|
+
if args.command == "init":
|
|
147
|
+
result = init_repo(client, args.name, args.description, args.visibility)
|
|
148
|
+
print(f"Created repository: {result['repository']['id']}")
|
|
149
|
+
elif args.command == "clone":
|
|
150
|
+
clone_repo(client, args.repo_id, args.target_dir)
|
|
151
|
+
elif args.command == "commit":
|
|
152
|
+
commit(client, args.message, args.files)
|
|
153
|
+
elif args.command == "push":
|
|
154
|
+
push(client, args.branch)
|