foodforthought-cli 0.2.0__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.
Files changed (46) hide show
  1. ate/__init__.py +1 -1
  2. ate/bridge_server.py +622 -0
  3. ate/cli.py +2625 -242
  4. ate/compatibility.py +580 -0
  5. ate/generators/__init__.py +19 -0
  6. ate/generators/docker_generator.py +461 -0
  7. ate/generators/hardware_config.py +469 -0
  8. ate/generators/ros2_generator.py +617 -0
  9. ate/generators/skill_generator.py +783 -0
  10. ate/marketplace.py +524 -0
  11. ate/mcp_server.py +2424 -148
  12. ate/primitives.py +1016 -0
  13. ate/robot_setup.py +2222 -0
  14. ate/skill_schema.py +537 -0
  15. ate/telemetry/__init__.py +33 -0
  16. ate/telemetry/cli.py +455 -0
  17. ate/telemetry/collector.py +444 -0
  18. ate/telemetry/context.py +318 -0
  19. ate/telemetry/fleet_agent.py +419 -0
  20. ate/telemetry/formats/__init__.py +18 -0
  21. ate/telemetry/formats/hdf5_serializer.py +503 -0
  22. ate/telemetry/formats/mcap_serializer.py +457 -0
  23. ate/telemetry/types.py +334 -0
  24. foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
  25. foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
  26. foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
  27. mechdog_labeled/__init__.py +3 -0
  28. mechdog_labeled/primitives.py +113 -0
  29. mechdog_labeled/servo_map.py +209 -0
  30. mechdog_output/__init__.py +3 -0
  31. mechdog_output/primitives.py +59 -0
  32. mechdog_output/servo_map.py +203 -0
  33. test_autodetect/__init__.py +3 -0
  34. test_autodetect/primitives.py +113 -0
  35. test_autodetect/servo_map.py +209 -0
  36. test_full_auto/__init__.py +3 -0
  37. test_full_auto/primitives.py +113 -0
  38. test_full_auto/servo_map.py +209 -0
  39. test_smart_detect/__init__.py +3 -0
  40. test_smart_detect/primitives.py +113 -0
  41. test_smart_detect/servo_map.py +209 -0
  42. foodforthought_cli-0.2.0.dist-info/METADATA +0 -151
  43. foodforthought_cli-0.2.0.dist-info/RECORD +0 -9
  44. foodforthought_cli-0.2.0.dist-info/top_level.txt +0 -1
  45. {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
  46. {foodforthought_cli-0.2.0.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")