foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.3__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/bridge_server.py +622 -0
- ate/cli.py +2625 -242
- ate/compatibility.py +580 -0
- ate/generators/__init__.py +19 -0
- ate/generators/docker_generator.py +461 -0
- ate/generators/hardware_config.py +469 -0
- ate/generators/ros2_generator.py +617 -0
- ate/generators/skill_generator.py +783 -0
- ate/marketplace.py +524 -0
- ate/mcp_server.py +1341 -107
- ate/primitives.py +1016 -0
- ate/robot_setup.py +2222 -0
- ate/skill_schema.py +537 -0
- ate/telemetry/__init__.py +33 -0
- ate/telemetry/cli.py +455 -0
- ate/telemetry/collector.py +444 -0
- ate/telemetry/context.py +318 -0
- ate/telemetry/fleet_agent.py +419 -0
- ate/telemetry/formats/__init__.py +18 -0
- ate/telemetry/formats/hdf5_serializer.py +503 -0
- ate/telemetry/formats/mcap_serializer.py +457 -0
- ate/telemetry/types.py +334 -0
- foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
- foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
- foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
- mechdog_labeled/__init__.py +3 -0
- mechdog_labeled/primitives.py +113 -0
- mechdog_labeled/servo_map.py +209 -0
- mechdog_output/__init__.py +3 -0
- mechdog_output/primitives.py +59 -0
- mechdog_output/servo_map.py +203 -0
- test_autodetect/__init__.py +3 -0
- test_autodetect/primitives.py +113 -0
- test_autodetect/servo_map.py +209 -0
- test_full_auto/__init__.py +3 -0
- test_full_auto/primitives.py +113 -0
- test_full_auto/servo_map.py +209 -0
- test_smart_detect/__init__.py +3 -0
- test_smart_detect/primitives.py +113 -0
- test_smart_detect/servo_map.py +209 -0
- foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/entry_points.txt +0 -0
ate/telemetry/cli.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Telemetry CLI Commands
|
|
3
|
+
|
|
4
|
+
Provides command-line interface for telemetry operations:
|
|
5
|
+
- Status checking
|
|
6
|
+
- Recording management
|
|
7
|
+
- Upload/download
|
|
8
|
+
- Fleet agent control
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from .collector import TelemetryCollector
|
|
20
|
+
from .types import TrajectoryRecording, TelemetrySource
|
|
21
|
+
from .fleet_agent import FleetTelemetryAgent, run_fleet_agent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_telemetry_subparsers(subparsers) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Add telemetry subparsers to the main CLI parser.
|
|
27
|
+
|
|
28
|
+
Usage in main cli.py:
|
|
29
|
+
from ate.telemetry.cli import add_telemetry_subparsers
|
|
30
|
+
add_telemetry_subparsers(subparsers)
|
|
31
|
+
"""
|
|
32
|
+
telemetry_parser = subparsers.add_parser(
|
|
33
|
+
"telemetry",
|
|
34
|
+
help="Telemetry data management",
|
|
35
|
+
description="""Manage telemetry data collection, upload, and analysis.
|
|
36
|
+
|
|
37
|
+
EXAMPLES:
|
|
38
|
+
ate telemetry status
|
|
39
|
+
ate telemetry upload recording.json
|
|
40
|
+
ate telemetry list --robot my-robot
|
|
41
|
+
ate telemetry agent my-robot --daemon
|
|
42
|
+
""",
|
|
43
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
44
|
+
)
|
|
45
|
+
telemetry_subparsers = telemetry_parser.add_subparsers(
|
|
46
|
+
dest="telemetry_action", help="Telemetry action"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# telemetry status
|
|
50
|
+
telemetry_subparsers.add_parser(
|
|
51
|
+
"status",
|
|
52
|
+
help="Show telemetry collection status and configuration",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# telemetry upload
|
|
56
|
+
upload_parser = telemetry_subparsers.add_parser(
|
|
57
|
+
"upload",
|
|
58
|
+
help="Upload a telemetry file to FoodforThought",
|
|
59
|
+
)
|
|
60
|
+
upload_parser.add_argument("file", help="Path to telemetry file (JSON, MCAP, or HDF5)")
|
|
61
|
+
upload_parser.add_argument(
|
|
62
|
+
"-f", "--format", default="auto",
|
|
63
|
+
choices=["auto", "json", "mcap", "hdf5"],
|
|
64
|
+
help="File format (default: auto-detect)",
|
|
65
|
+
)
|
|
66
|
+
upload_parser.add_argument("--robot-id", help="Override robot ID in recording")
|
|
67
|
+
upload_parser.add_argument("--skill-id", help="Associate with skill ID")
|
|
68
|
+
upload_parser.add_argument("--project-id", help="FoodforThought project ID")
|
|
69
|
+
upload_parser.add_argument(
|
|
70
|
+
"--tags", help="Comma-separated tags to add"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# telemetry export
|
|
74
|
+
export_parser = telemetry_subparsers.add_parser(
|
|
75
|
+
"export",
|
|
76
|
+
help="Download and export telemetry from FoodforThought",
|
|
77
|
+
)
|
|
78
|
+
export_parser.add_argument("artifact_id", help="Artifact ID to download")
|
|
79
|
+
export_parser.add_argument(
|
|
80
|
+
"-o", "--output", default=".",
|
|
81
|
+
help="Output directory (default: current directory)",
|
|
82
|
+
)
|
|
83
|
+
export_parser.add_argument(
|
|
84
|
+
"-f", "--format", default="mcap",
|
|
85
|
+
choices=["json", "mcap", "hdf5", "csv"],
|
|
86
|
+
help="Export format (default: mcap)",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# telemetry list
|
|
90
|
+
list_parser = telemetry_subparsers.add_parser(
|
|
91
|
+
"list",
|
|
92
|
+
help="List telemetry recordings from FoodforThought",
|
|
93
|
+
)
|
|
94
|
+
list_parser.add_argument("--robot-id", help="Filter by robot ID")
|
|
95
|
+
list_parser.add_argument("--skill-id", help="Filter by skill ID")
|
|
96
|
+
list_parser.add_argument(
|
|
97
|
+
"--source", choices=["simulation", "hardware", "fleet"],
|
|
98
|
+
help="Filter by source",
|
|
99
|
+
)
|
|
100
|
+
list_parser.add_argument("--success", type=bool, help="Filter by success status")
|
|
101
|
+
list_parser.add_argument("--limit", type=int, default=20, help="Max results")
|
|
102
|
+
list_parser.add_argument(
|
|
103
|
+
"--format", choices=["table", "json"], default="table",
|
|
104
|
+
help="Output format",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# telemetry agent
|
|
108
|
+
agent_parser = telemetry_subparsers.add_parser(
|
|
109
|
+
"agent",
|
|
110
|
+
help="Start the fleet telemetry agent",
|
|
111
|
+
description="""Start a background agent for continuous telemetry collection.
|
|
112
|
+
|
|
113
|
+
The agent runs on each fleet robot, collecting state data at a configurable
|
|
114
|
+
frequency and periodically uploading to FoodforThought.
|
|
115
|
+
|
|
116
|
+
EXAMPLES:
|
|
117
|
+
ate telemetry agent my-robot-001
|
|
118
|
+
ate telemetry agent my-robot-001 --daemon
|
|
119
|
+
ate telemetry agent my-robot-001 --collection-hz 100 --upload-interval 30
|
|
120
|
+
""",
|
|
121
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
122
|
+
)
|
|
123
|
+
agent_parser.add_argument("robot_id", help="Unique robot identifier")
|
|
124
|
+
agent_parser.add_argument(
|
|
125
|
+
"--daemon", action="store_true",
|
|
126
|
+
help="Run as daemon (detach from terminal)",
|
|
127
|
+
)
|
|
128
|
+
agent_parser.add_argument(
|
|
129
|
+
"--collection-hz", type=float, default=50.0,
|
|
130
|
+
help="Collection frequency in Hz (default: 50)",
|
|
131
|
+
)
|
|
132
|
+
agent_parser.add_argument(
|
|
133
|
+
"--upload-interval", type=float, default=60.0,
|
|
134
|
+
help="Upload interval in seconds (default: 60)",
|
|
135
|
+
)
|
|
136
|
+
agent_parser.add_argument("--api-key", help="FoodforThought API key")
|
|
137
|
+
agent_parser.add_argument("--project-id", help="FoodforThought project ID")
|
|
138
|
+
|
|
139
|
+
# telemetry record
|
|
140
|
+
record_parser = telemetry_subparsers.add_parser(
|
|
141
|
+
"record",
|
|
142
|
+
help="Record telemetry from stdin (pipe from robot)",
|
|
143
|
+
description="""Record telemetry data from stdin.
|
|
144
|
+
|
|
145
|
+
Useful for piping robot state data directly to a recording.
|
|
146
|
+
|
|
147
|
+
EXAMPLES:
|
|
148
|
+
robot_state_stream | ate telemetry record my-robot --skill pick_and_place
|
|
149
|
+
ate telemetry record my-robot < state_dump.jsonl
|
|
150
|
+
""",
|
|
151
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
152
|
+
)
|
|
153
|
+
record_parser.add_argument("robot_id", help="Robot identifier")
|
|
154
|
+
record_parser.add_argument("--skill-id", help="Associated skill ID")
|
|
155
|
+
record_parser.add_argument(
|
|
156
|
+
"-o", "--output", help="Save recording to file (default: upload to FFT)"
|
|
157
|
+
)
|
|
158
|
+
record_parser.add_argument(
|
|
159
|
+
"-f", "--format", default="json",
|
|
160
|
+
choices=["json", "mcap", "hdf5"],
|
|
161
|
+
help="Output format (default: json)",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def handle_telemetry_command(args, client) -> None:
|
|
166
|
+
"""Handle telemetry subcommands."""
|
|
167
|
+
if args.telemetry_action == "status":
|
|
168
|
+
_handle_status(args, client)
|
|
169
|
+
elif args.telemetry_action == "upload":
|
|
170
|
+
_handle_upload(args, client)
|
|
171
|
+
elif args.telemetry_action == "export":
|
|
172
|
+
_handle_export(args, client)
|
|
173
|
+
elif args.telemetry_action == "list":
|
|
174
|
+
_handle_list(args, client)
|
|
175
|
+
elif args.telemetry_action == "agent":
|
|
176
|
+
_handle_agent(args)
|
|
177
|
+
elif args.telemetry_action == "record":
|
|
178
|
+
_handle_record(args, client)
|
|
179
|
+
else:
|
|
180
|
+
print("Please specify a telemetry action. Use 'ate telemetry --help' for options.")
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _handle_status(args, client) -> None:
|
|
185
|
+
"""Show telemetry status and configuration."""
|
|
186
|
+
api_key = os.getenv("FFT_API_KEY") or os.getenv("ATE_API_KEY")
|
|
187
|
+
api_url = os.getenv("FFT_API_URL", "https://kindly.fyi/api")
|
|
188
|
+
project_id = os.getenv("FFT_PROJECT_ID")
|
|
189
|
+
|
|
190
|
+
print("\n=== Telemetry Configuration ===\n")
|
|
191
|
+
print(f"API URL: {api_url}")
|
|
192
|
+
print(f"API Key: {'***' + api_key[-8:] if api_key else 'Not set'}")
|
|
193
|
+
print(f"Project ID: {project_id or 'Not set'}")
|
|
194
|
+
print()
|
|
195
|
+
|
|
196
|
+
if api_key:
|
|
197
|
+
# Try to fetch recent recordings
|
|
198
|
+
try:
|
|
199
|
+
response = client._request("GET", "/telemetry/query", params={"limit": 5})
|
|
200
|
+
recordings = response.get("data", {}).get("recordings", [])
|
|
201
|
+
|
|
202
|
+
print("=== Recent Recordings ===\n")
|
|
203
|
+
if recordings:
|
|
204
|
+
for rec in recordings:
|
|
205
|
+
status = "✓" if rec.get("success") else "✗"
|
|
206
|
+
print(f" {status} {rec.get('name')} - {rec.get('source')} - {rec.get('duration', 0):.1f}s")
|
|
207
|
+
else:
|
|
208
|
+
print(" No recordings found")
|
|
209
|
+
print()
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(f"Could not fetch recordings: {e}")
|
|
212
|
+
else:
|
|
213
|
+
print("Set FFT_API_KEY or ATE_API_KEY to enable telemetry uploads.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _handle_upload(args, client) -> None:
|
|
217
|
+
"""Upload a telemetry file."""
|
|
218
|
+
filepath = Path(args.file)
|
|
219
|
+
if not filepath.exists():
|
|
220
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
# Detect format
|
|
224
|
+
format = args.format
|
|
225
|
+
if format == "auto":
|
|
226
|
+
ext = filepath.suffix.lower()
|
|
227
|
+
if ext == ".json":
|
|
228
|
+
format = "json"
|
|
229
|
+
elif ext in [".mcap", ".bag"]:
|
|
230
|
+
format = "mcap"
|
|
231
|
+
elif ext in [".h5", ".hdf5"]:
|
|
232
|
+
format = "hdf5"
|
|
233
|
+
else:
|
|
234
|
+
print(f"Error: Could not detect format. Use --format to specify.", file=sys.stderr)
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
print(f"Uploading {filepath} ({format} format)...")
|
|
238
|
+
|
|
239
|
+
# Load recording
|
|
240
|
+
try:
|
|
241
|
+
if format == "json":
|
|
242
|
+
with open(filepath) as f:
|
|
243
|
+
data = json.load(f)
|
|
244
|
+
|
|
245
|
+
# Build recording from JSON
|
|
246
|
+
recording_data = data.get("recording") or data
|
|
247
|
+
if args.robot_id:
|
|
248
|
+
recording_data["robotId"] = args.robot_id
|
|
249
|
+
if args.skill_id:
|
|
250
|
+
recording_data["skillId"] = args.skill_id
|
|
251
|
+
|
|
252
|
+
elif format == "mcap":
|
|
253
|
+
from .formats.mcap_serializer import deserialize_from_mcap
|
|
254
|
+
with open(filepath, "rb") as f:
|
|
255
|
+
mcap_data = f.read()
|
|
256
|
+
recording = deserialize_from_mcap(mcap_data)
|
|
257
|
+
recording_data = recording.to_dict()
|
|
258
|
+
|
|
259
|
+
elif format == "hdf5":
|
|
260
|
+
from .formats.hdf5_serializer import deserialize_from_hdf5
|
|
261
|
+
with open(filepath, "rb") as f:
|
|
262
|
+
hdf5_data = f.read()
|
|
263
|
+
recording = deserialize_from_hdf5(hdf5_data)
|
|
264
|
+
recording_data = recording.to_dict()
|
|
265
|
+
|
|
266
|
+
# Add tags if specified
|
|
267
|
+
if args.tags:
|
|
268
|
+
tags = [t.strip() for t in args.tags.split(",")]
|
|
269
|
+
if "metadata" not in recording_data:
|
|
270
|
+
recording_data["metadata"] = {}
|
|
271
|
+
recording_data["metadata"]["tags"] = tags
|
|
272
|
+
|
|
273
|
+
# Upload
|
|
274
|
+
response = client._request("POST", "/telemetry/ingest", json={
|
|
275
|
+
"recording": recording_data,
|
|
276
|
+
"projectId": args.project_id,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
result = response.get("data", {})
|
|
280
|
+
print(f"\n✓ Uploaded successfully!")
|
|
281
|
+
print(f" Artifact ID: {result.get('artifactId')}")
|
|
282
|
+
print(f" Frames: {result.get('frameCount')}")
|
|
283
|
+
print(f" Duration: {result.get('duration', 0):.2f}s")
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
print(f"Error uploading: {e}", file=sys.stderr)
|
|
287
|
+
sys.exit(1)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _handle_export(args, client) -> None:
|
|
291
|
+
"""Download and export telemetry."""
|
|
292
|
+
print(f"Downloading artifact {args.artifact_id}...")
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
response = client._request("GET", f"/telemetry/recordings/{args.artifact_id}")
|
|
296
|
+
data = response.get("data", {})
|
|
297
|
+
|
|
298
|
+
if not data:
|
|
299
|
+
print(f"Error: Artifact not found", file=sys.stderr)
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
# Build output path
|
|
303
|
+
output_dir = Path(args.output)
|
|
304
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
filename = f"telemetry_{args.artifact_id}.{args.format}"
|
|
306
|
+
output_path = output_dir / filename
|
|
307
|
+
|
|
308
|
+
# Export to requested format
|
|
309
|
+
if args.format == "json":
|
|
310
|
+
with open(output_path, "w") as f:
|
|
311
|
+
json.dump(data, f, indent=2, default=str)
|
|
312
|
+
elif args.format == "csv":
|
|
313
|
+
# Simple CSV export of key metrics
|
|
314
|
+
import csv
|
|
315
|
+
with open(output_path, "w", newline="") as f:
|
|
316
|
+
writer = csv.writer(f)
|
|
317
|
+
writer.writerow(["id", "robotId", "skillId", "source", "success", "duration", "frameCount"])
|
|
318
|
+
writer.writerow([
|
|
319
|
+
data.get("id"),
|
|
320
|
+
data.get("robotId"),
|
|
321
|
+
data.get("skillId"),
|
|
322
|
+
data.get("source"),
|
|
323
|
+
data.get("success"),
|
|
324
|
+
data.get("duration"),
|
|
325
|
+
data.get("frameCount"),
|
|
326
|
+
])
|
|
327
|
+
else:
|
|
328
|
+
print(f"Format {args.format} export not yet implemented for remote data")
|
|
329
|
+
sys.exit(1)
|
|
330
|
+
|
|
331
|
+
print(f"✓ Exported to {output_path}")
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
print(f"Error exporting: {e}", file=sys.stderr)
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _handle_list(args, client) -> None:
|
|
339
|
+
"""List telemetry recordings."""
|
|
340
|
+
params = {
|
|
341
|
+
"limit": args.limit,
|
|
342
|
+
}
|
|
343
|
+
if args.robot_id:
|
|
344
|
+
params["robotId"] = args.robot_id
|
|
345
|
+
if args.skill_id:
|
|
346
|
+
params["skillId"] = args.skill_id
|
|
347
|
+
if args.source:
|
|
348
|
+
params["source"] = args.source
|
|
349
|
+
if args.success is not None:
|
|
350
|
+
params["success"] = str(args.success).lower()
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
response = client._request("GET", "/telemetry/query", params=params)
|
|
354
|
+
data = response.get("data", {})
|
|
355
|
+
recordings = data.get("recordings", [])
|
|
356
|
+
|
|
357
|
+
if args.format == "json":
|
|
358
|
+
print(json.dumps(recordings, indent=2, default=str))
|
|
359
|
+
else:
|
|
360
|
+
# Table format
|
|
361
|
+
print(f"\n{'ID':<15} {'Robot':<15} {'Skill':<15} {'Source':<12} {'Status':<8} {'Duration':<10} {'Frames':<8}")
|
|
362
|
+
print("-" * 95)
|
|
363
|
+
|
|
364
|
+
for rec in recordings:
|
|
365
|
+
status = "✓" if rec.get("success") else "✗"
|
|
366
|
+
print(
|
|
367
|
+
f"{rec.get('id', '')[:14]:<15} "
|
|
368
|
+
f"{rec.get('robotId', '')[:14]:<15} "
|
|
369
|
+
f"{(rec.get('skillId') or '-')[:14]:<15} "
|
|
370
|
+
f"{rec.get('source', ''):<12} "
|
|
371
|
+
f"{status:<8} "
|
|
372
|
+
f"{rec.get('duration', 0):.1f}s{'':<5} "
|
|
373
|
+
f"{rec.get('frameCount', 0):<8}"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
print(f"\nTotal: {data.get('total', len(recordings))} recordings")
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
print(f"Error listing recordings: {e}", file=sys.stderr)
|
|
380
|
+
sys.exit(1)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _handle_agent(args) -> None:
|
|
384
|
+
"""Start the fleet telemetry agent."""
|
|
385
|
+
print(f"Starting fleet telemetry agent for robot: {args.robot_id}")
|
|
386
|
+
print(f"Collection frequency: {args.collection_hz} Hz")
|
|
387
|
+
print(f"Upload interval: {args.upload_interval}s")
|
|
388
|
+
|
|
389
|
+
if args.daemon:
|
|
390
|
+
print("Running as daemon...")
|
|
391
|
+
|
|
392
|
+
asyncio.run(run_fleet_agent(
|
|
393
|
+
robot_id=args.robot_id,
|
|
394
|
+
api_key=args.api_key,
|
|
395
|
+
collection_hz=args.collection_hz,
|
|
396
|
+
upload_interval=args.upload_interval,
|
|
397
|
+
daemon=args.daemon,
|
|
398
|
+
))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _handle_record(args, client) -> None:
|
|
402
|
+
"""Record telemetry from stdin."""
|
|
403
|
+
print(f"Recording telemetry for robot: {args.robot_id}")
|
|
404
|
+
print("Reading from stdin... (Ctrl+C to stop)")
|
|
405
|
+
|
|
406
|
+
collector = TelemetryCollector(
|
|
407
|
+
robot_id=args.robot_id,
|
|
408
|
+
auto_upload=args.output is None,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
collector.start_recording(
|
|
412
|
+
skill_id=args.skill_id,
|
|
413
|
+
source="hardware",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
for line in sys.stdin:
|
|
418
|
+
line = line.strip()
|
|
419
|
+
if not line:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
data = json.loads(line)
|
|
424
|
+
|
|
425
|
+
# Extract joint positions from various formats
|
|
426
|
+
joint_positions = {}
|
|
427
|
+
if "positions" in data:
|
|
428
|
+
joint_positions = data["positions"]
|
|
429
|
+
elif "joint_positions" in data:
|
|
430
|
+
joint_positions = data["joint_positions"]
|
|
431
|
+
elif "qpos" in data:
|
|
432
|
+
qpos = data["qpos"]
|
|
433
|
+
joint_positions = {f"joint_{i}": v for i, v in enumerate(qpos)}
|
|
434
|
+
|
|
435
|
+
if joint_positions:
|
|
436
|
+
collector.record_frame(
|
|
437
|
+
joint_positions=joint_positions,
|
|
438
|
+
joint_velocities=data.get("velocities") or data.get("joint_velocities"),
|
|
439
|
+
joint_torques=data.get("torques") or data.get("joint_torques"),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
except json.JSONDecodeError:
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
except KeyboardInterrupt:
|
|
446
|
+
print("\nStopping recording...")
|
|
447
|
+
|
|
448
|
+
recording = collector.stop_recording(success=True)
|
|
449
|
+
|
|
450
|
+
if args.output:
|
|
451
|
+
# Save to file
|
|
452
|
+
collector.export_to_file(recording, args.output, args.format)
|
|
453
|
+
print(f"✓ Saved to {args.output}")
|
|
454
|
+
else:
|
|
455
|
+
print(f"✓ Uploaded {recording.metadata.total_frames} frames")
|