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.
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 +1341 -107
  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.1.dist-info/METADATA +0 -151
  43. foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
  44. foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
  45. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
  46. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,419 @@
1
+ """
2
+ Fleet Telemetry Agent
3
+
4
+ Background agent for continuous telemetry collection from deployed fleet robots.
5
+ Runs on each robot to collect and periodically upload telemetry to FoodforThought.
6
+
7
+ Usage:
8
+ agent = FleetTelemetryAgent(
9
+ robot_id="robot-001",
10
+ api_key="fft_xxx",
11
+ )
12
+ await agent.start()
13
+ """
14
+
15
+ import asyncio
16
+ import os
17
+ import sys
18
+ import signal
19
+ import time
20
+ from datetime import datetime
21
+ from typing import Callable, Dict, List, Optional, Any
22
+ from uuid import uuid4
23
+
24
+ from .collector import TelemetryCollector
25
+ from .types import (
26
+ TrajectoryRecording,
27
+ TrajectoryFrame,
28
+ TrajectoryMetadata,
29
+ TelemetrySource,
30
+ TelemetryBuffer,
31
+ ExecutionEvent,
32
+ EventType,
33
+ )
34
+
35
+
36
+ class FleetTelemetryAgent:
37
+ """
38
+ Background agent for fleet robot telemetry collection.
39
+
40
+ Runs continuously on a deployed robot, collecting state data and
41
+ periodically uploading to FoodforThought for analysis and training.
42
+
43
+ Features:
44
+ - Configurable collection frequency
45
+ - Automatic batching and upload
46
+ - Graceful shutdown handling
47
+ - State provider callback for robot-specific integration
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ robot_id: str,
53
+ api_url: str = None,
54
+ api_key: str = None,
55
+ collection_interval: float = 0.02, # 50Hz
56
+ upload_interval: float = 60.0, # Upload every minute
57
+ buffer_size: int = 3600, # 1 minute at 50Hz
58
+ project_id: str = None,
59
+ state_provider: Callable[[], Dict[str, float]] = None,
60
+ velocity_provider: Callable[[], Dict[str, float]] = None,
61
+ torque_provider: Callable[[], Dict[str, float]] = None,
62
+ ):
63
+ """
64
+ Initialize the fleet telemetry agent.
65
+
66
+ Args:
67
+ robot_id: Unique identifier for this robot
68
+ api_url: FoodforThought API URL
69
+ api_key: API key for authentication
70
+ collection_interval: Time between state samples (seconds)
71
+ upload_interval: Time between uploads (seconds)
72
+ buffer_size: Maximum frames to buffer
73
+ project_id: FFT project ID for artifacts
74
+ state_provider: Callback returning joint positions dict
75
+ velocity_provider: Callback returning joint velocities dict
76
+ torque_provider: Callback returning joint torques dict
77
+ """
78
+ self.robot_id = robot_id
79
+ self.api_url = api_url or os.getenv("FFT_API_URL", "https://kindly.fyi/api")
80
+ self.api_key = api_key or os.getenv("FFT_API_KEY") or os.getenv("ATE_API_KEY")
81
+ self.project_id = project_id or os.getenv("FFT_PROJECT_ID")
82
+
83
+ self.collection_interval = collection_interval
84
+ self.upload_interval = upload_interval
85
+ self.buffer_size = buffer_size
86
+
87
+ self.state_provider = state_provider
88
+ self.velocity_provider = velocity_provider
89
+ self.torque_provider = torque_provider
90
+
91
+ self.collector = TelemetryCollector(
92
+ robot_id=robot_id,
93
+ api_url=self.api_url,
94
+ api_key=self.api_key,
95
+ buffer_size=buffer_size,
96
+ auto_upload=False, # We handle uploads manually
97
+ project_id=project_id,
98
+ )
99
+
100
+ self._running = False
101
+ self._buffer = TelemetryBuffer(max_size=buffer_size)
102
+ self._start_time: Optional[datetime] = None
103
+ self._frame_count = 0
104
+ self._upload_count = 0
105
+ self._last_upload_time: Optional[float] = None
106
+
107
+ # Setup signal handlers for graceful shutdown
108
+ signal.signal(signal.SIGINT, self._signal_handler)
109
+ signal.signal(signal.SIGTERM, self._signal_handler)
110
+
111
+ def _signal_handler(self, signum, frame):
112
+ """Handle shutdown signals."""
113
+ print(f"\n[FleetAgent] Received signal {signum}, shutting down...")
114
+ self.stop()
115
+
116
+ async def start(self) -> None:
117
+ """Start the telemetry agent."""
118
+ if self._running:
119
+ print("[FleetAgent] Already running")
120
+ return
121
+
122
+ if not self.api_key:
123
+ print("[FleetAgent] Warning: No API key configured, telemetry will not be uploaded",
124
+ file=sys.stderr)
125
+
126
+ self._running = True
127
+ self._start_time = datetime.utcnow()
128
+ self._frame_count = 0
129
+ self._upload_count = 0
130
+
131
+ print(f"[FleetAgent] Starting telemetry collection for robot {self.robot_id}")
132
+ print(f"[FleetAgent] Collection interval: {self.collection_interval}s ({1/self.collection_interval:.1f}Hz)")
133
+ print(f"[FleetAgent] Upload interval: {self.upload_interval}s")
134
+
135
+ # Start collection and upload tasks
136
+ await asyncio.gather(
137
+ self._collection_loop(),
138
+ self._upload_loop(),
139
+ )
140
+
141
+ def stop(self) -> None:
142
+ """Stop the telemetry agent."""
143
+ self._running = False
144
+
145
+ # Upload any remaining data
146
+ if not self._buffer.is_empty():
147
+ print(f"[FleetAgent] Uploading final {self._buffer.frame_count} frames...")
148
+ asyncio.create_task(self._upload_buffer())
149
+
150
+ print(f"[FleetAgent] Stopped. Collected {self._frame_count} frames, {self._upload_count} uploads.")
151
+
152
+ async def _collection_loop(self) -> None:
153
+ """Main collection loop - samples state at configured frequency."""
154
+ while self._running:
155
+ try:
156
+ # Get current state from providers
157
+ state = await self._get_robot_state()
158
+
159
+ if state:
160
+ self._buffer.add(state)
161
+ self._frame_count += 1
162
+
163
+ await asyncio.sleep(self.collection_interval)
164
+
165
+ except Exception as e:
166
+ print(f"[FleetAgent] Collection error: {e}", file=sys.stderr)
167
+ await asyncio.sleep(1.0) # Back off on error
168
+
169
+ async def _upload_loop(self) -> None:
170
+ """Upload loop - periodically uploads buffered data."""
171
+ while self._running:
172
+ await asyncio.sleep(self.upload_interval)
173
+
174
+ if not self._buffer.is_empty():
175
+ await self._upload_buffer()
176
+
177
+ async def _get_robot_state(self) -> Optional[TrajectoryFrame]:
178
+ """Get current robot state from providers."""
179
+ try:
180
+ joint_positions = {}
181
+ joint_velocities = {}
182
+ joint_torques = {}
183
+
184
+ if self.state_provider:
185
+ result = self.state_provider()
186
+ if asyncio.iscoroutine(result):
187
+ joint_positions = await result
188
+ else:
189
+ joint_positions = result
190
+
191
+ if self.velocity_provider:
192
+ result = self.velocity_provider()
193
+ if asyncio.iscoroutine(result):
194
+ joint_velocities = await result
195
+ else:
196
+ joint_velocities = result
197
+
198
+ if self.torque_provider:
199
+ result = self.torque_provider()
200
+ if asyncio.iscoroutine(result):
201
+ joint_torques = await result
202
+ else:
203
+ joint_torques = result
204
+
205
+ # Calculate timestamp from start
206
+ elapsed = (datetime.utcnow() - self._start_time).total_seconds()
207
+
208
+ return TrajectoryFrame(
209
+ timestamp=elapsed,
210
+ joint_positions=joint_positions,
211
+ joint_velocities=joint_velocities,
212
+ joint_torques=joint_torques,
213
+ )
214
+
215
+ except Exception as e:
216
+ print(f"[FleetAgent] State provider error: {e}", file=sys.stderr)
217
+ return None
218
+
219
+ async def _upload_buffer(self) -> None:
220
+ """Upload buffered frames to FoodforThought."""
221
+ if self._buffer.is_empty():
222
+ return
223
+
224
+ frames, events = self._buffer.flush()
225
+
226
+ # Create recording from buffered frames
227
+ recording_id = str(uuid4())
228
+ now = datetime.utcnow()
229
+
230
+ recording = TrajectoryRecording(
231
+ id=recording_id,
232
+ robot_id=self.robot_id,
233
+ source=TelemetrySource.FLEET,
234
+ start_time=now,
235
+ end_time=now,
236
+ success=True,
237
+ frames=frames,
238
+ events=events,
239
+ metadata=TrajectoryMetadata(
240
+ duration=self.upload_interval,
241
+ total_frames=len(frames),
242
+ frame_rate=len(frames) / self.upload_interval if self.upload_interval > 0 else 0,
243
+ tags=["fleet", "continuous"],
244
+ ),
245
+ )
246
+
247
+ # Upload using collector
248
+ result = self.collector._upload_recording_sync(recording)
249
+
250
+ if result:
251
+ self._upload_count += 1
252
+ self._last_upload_time = time.time()
253
+ print(f"[FleetAgent] Uploaded {len(frames)} frames (total: {self._upload_count})")
254
+
255
+ def log_event(self, event_type: EventType, data: Optional[Dict[str, Any]] = None) -> None:
256
+ """Log an execution event."""
257
+ elapsed = (datetime.utcnow() - self._start_time).total_seconds() if self._start_time else 0
258
+
259
+ event = ExecutionEvent(
260
+ timestamp=elapsed,
261
+ event_type=event_type,
262
+ data=data or {},
263
+ )
264
+ self._buffer.add_event(event)
265
+
266
+ def get_status(self) -> Dict[str, Any]:
267
+ """Get current agent status."""
268
+ uptime = 0
269
+ if self._start_time:
270
+ uptime = (datetime.utcnow() - self._start_time).total_seconds()
271
+
272
+ return {
273
+ "running": self._running,
274
+ "robot_id": self.robot_id,
275
+ "uptime": uptime,
276
+ "frame_count": self._frame_count,
277
+ "upload_count": self._upload_count,
278
+ "buffer_size": self._buffer.frame_count,
279
+ "last_upload": self._last_upload_time,
280
+ "collection_hz": 1 / self.collection_interval if self.collection_interval > 0 else 0,
281
+ }
282
+
283
+
284
+ # ============================================================================
285
+ # ROS2 Integration
286
+ # ============================================================================
287
+
288
+ class ROS2TelemetryAgent(FleetTelemetryAgent):
289
+ """
290
+ Fleet telemetry agent with ROS2 integration.
291
+
292
+ Subscribes to joint_states topic for automatic state collection.
293
+ """
294
+
295
+ def __init__(
296
+ self,
297
+ robot_id: str,
298
+ joint_states_topic: str = "/joint_states",
299
+ **kwargs,
300
+ ):
301
+ """
302
+ Initialize ROS2 telemetry agent.
303
+
304
+ Args:
305
+ robot_id: Unique identifier for this robot
306
+ joint_states_topic: ROS2 topic for joint states
307
+ **kwargs: Additional arguments passed to FleetTelemetryAgent
308
+ """
309
+ super().__init__(robot_id, **kwargs)
310
+ self.joint_states_topic = joint_states_topic
311
+ self._latest_state: Dict[str, float] = {}
312
+ self._latest_velocity: Dict[str, float] = {}
313
+ self._latest_effort: Dict[str, float] = {}
314
+
315
+ # Set providers to use ROS2 state
316
+ self.state_provider = self._get_ros2_positions
317
+ self.velocity_provider = self._get_ros2_velocities
318
+ self.torque_provider = self._get_ros2_efforts
319
+
320
+ def _get_ros2_positions(self) -> Dict[str, float]:
321
+ """Get positions from ROS2 joint_states."""
322
+ return self._latest_state.copy()
323
+
324
+ def _get_ros2_velocities(self) -> Dict[str, float]:
325
+ """Get velocities from ROS2 joint_states."""
326
+ return self._latest_velocity.copy()
327
+
328
+ def _get_ros2_efforts(self) -> Dict[str, float]:
329
+ """Get efforts from ROS2 joint_states."""
330
+ return self._latest_effort.copy()
331
+
332
+ def update_joint_states(
333
+ self,
334
+ names: List[str],
335
+ positions: List[float],
336
+ velocities: Optional[List[float]] = None,
337
+ efforts: Optional[List[float]] = None,
338
+ ) -> None:
339
+ """
340
+ Update joint states from ROS2 callback.
341
+
342
+ Call this from your ROS2 joint_states subscriber callback.
343
+
344
+ Args:
345
+ names: Joint names
346
+ positions: Joint positions (radians)
347
+ velocities: Joint velocities (rad/s)
348
+ efforts: Joint efforts (Nm)
349
+ """
350
+ for i, name in enumerate(names):
351
+ if i < len(positions):
352
+ self._latest_state[name] = positions[i]
353
+ if velocities and i < len(velocities):
354
+ self._latest_velocity[name] = velocities[i]
355
+ if efforts and i < len(efforts):
356
+ self._latest_effort[name] = efforts[i]
357
+
358
+
359
+ # ============================================================================
360
+ # Standalone Runner
361
+ # ============================================================================
362
+
363
+ async def run_fleet_agent(
364
+ robot_id: str,
365
+ api_key: str = None,
366
+ collection_hz: float = 50.0,
367
+ upload_interval: float = 60.0,
368
+ daemon: bool = False,
369
+ ) -> None:
370
+ """
371
+ Run the fleet telemetry agent.
372
+
373
+ This is a convenience function for running the agent from the command line.
374
+
375
+ Args:
376
+ robot_id: Unique identifier for this robot
377
+ api_key: FoodforThought API key
378
+ collection_hz: Collection frequency in Hz
379
+ upload_interval: Upload interval in seconds
380
+ daemon: Run as daemon (detach from terminal)
381
+ """
382
+ if daemon:
383
+ # Daemonize the process
384
+ import os
385
+ pid = os.fork()
386
+ if pid > 0:
387
+ print(f"[FleetAgent] Started as daemon (PID: {pid})")
388
+ sys.exit(0)
389
+ os.setsid()
390
+
391
+ agent = FleetTelemetryAgent(
392
+ robot_id=robot_id,
393
+ api_key=api_key,
394
+ collection_interval=1.0 / collection_hz,
395
+ upload_interval=upload_interval,
396
+ )
397
+
398
+ await agent.start()
399
+
400
+
401
+ if __name__ == "__main__":
402
+ import argparse
403
+
404
+ parser = argparse.ArgumentParser(description="Fleet Telemetry Agent")
405
+ parser.add_argument("robot_id", help="Unique robot identifier")
406
+ parser.add_argument("--api-key", help="FoodforThought API key")
407
+ parser.add_argument("--collection-hz", type=float, default=50.0, help="Collection frequency")
408
+ parser.add_argument("--upload-interval", type=float, default=60.0, help="Upload interval")
409
+ parser.add_argument("--daemon", action="store_true", help="Run as daemon")
410
+
411
+ args = parser.parse_args()
412
+
413
+ asyncio.run(run_fleet_agent(
414
+ robot_id=args.robot_id,
415
+ api_key=args.api_key,
416
+ collection_hz=args.collection_hz,
417
+ upload_interval=args.upload_interval,
418
+ daemon=args.daemon,
419
+ ))
@@ -0,0 +1,18 @@
1
+ """
2
+ Format serializers for telemetry data.
3
+
4
+ Supports:
5
+ - MCAP: ROS ecosystem standard, Foxglove compatible
6
+ - HDF5: ML-friendly format for training pipelines
7
+ - JSON: Human-readable, debugging
8
+ """
9
+
10
+ from .mcap_serializer import serialize_to_mcap, deserialize_from_mcap
11
+ from .hdf5_serializer import serialize_to_hdf5, deserialize_from_hdf5
12
+
13
+ __all__ = [
14
+ "serialize_to_mcap",
15
+ "deserialize_from_mcap",
16
+ "serialize_to_hdf5",
17
+ "deserialize_from_hdf5",
18
+ ]