foodforthought-cli 0.2.7__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.
Files changed (131) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +100 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/behaviors/common.py +686 -0
  9. ate/behaviors/tree.py +454 -0
  10. ate/cli.py +855 -3995
  11. ate/client.py +90 -0
  12. ate/commands/__init__.py +168 -0
  13. ate/commands/auth.py +389 -0
  14. ate/commands/bridge.py +448 -0
  15. ate/commands/data.py +185 -0
  16. ate/commands/deps.py +111 -0
  17. ate/commands/generate.py +384 -0
  18. ate/commands/memory.py +907 -0
  19. ate/commands/parts.py +166 -0
  20. ate/commands/primitive.py +399 -0
  21. ate/commands/protocol.py +288 -0
  22. ate/commands/recording.py +524 -0
  23. ate/commands/repo.py +154 -0
  24. ate/commands/simulation.py +291 -0
  25. ate/commands/skill.py +303 -0
  26. ate/commands/skills.py +487 -0
  27. ate/commands/team.py +147 -0
  28. ate/commands/workflow.py +271 -0
  29. ate/detection/__init__.py +38 -0
  30. ate/detection/base.py +142 -0
  31. ate/detection/color_detector.py +399 -0
  32. ate/detection/trash_detector.py +322 -0
  33. ate/drivers/__init__.py +39 -0
  34. ate/drivers/ble_transport.py +405 -0
  35. ate/drivers/mechdog.py +942 -0
  36. ate/drivers/wifi_camera.py +477 -0
  37. ate/interfaces/__init__.py +187 -0
  38. ate/interfaces/base.py +273 -0
  39. ate/interfaces/body.py +267 -0
  40. ate/interfaces/detection.py +282 -0
  41. ate/interfaces/locomotion.py +422 -0
  42. ate/interfaces/manipulation.py +408 -0
  43. ate/interfaces/navigation.py +389 -0
  44. ate/interfaces/perception.py +362 -0
  45. ate/interfaces/sensors.py +247 -0
  46. ate/interfaces/types.py +371 -0
  47. ate/llm_proxy.py +239 -0
  48. ate/mcp_server.py +387 -0
  49. ate/memory/__init__.py +35 -0
  50. ate/memory/cloud.py +244 -0
  51. ate/memory/context.py +269 -0
  52. ate/memory/embeddings.py +184 -0
  53. ate/memory/export.py +26 -0
  54. ate/memory/merge.py +146 -0
  55. ate/memory/migrate/__init__.py +34 -0
  56. ate/memory/migrate/base.py +89 -0
  57. ate/memory/migrate/pipeline.py +189 -0
  58. ate/memory/migrate/sources/__init__.py +13 -0
  59. ate/memory/migrate/sources/chroma.py +170 -0
  60. ate/memory/migrate/sources/pinecone.py +120 -0
  61. ate/memory/migrate/sources/qdrant.py +110 -0
  62. ate/memory/migrate/sources/weaviate.py +160 -0
  63. ate/memory/reranker.py +353 -0
  64. ate/memory/search.py +26 -0
  65. ate/memory/store.py +548 -0
  66. ate/recording/__init__.py +83 -0
  67. ate/recording/demonstration.py +378 -0
  68. ate/recording/session.py +415 -0
  69. ate/recording/upload.py +304 -0
  70. ate/recording/visual.py +416 -0
  71. ate/recording/wrapper.py +95 -0
  72. ate/robot/__init__.py +221 -0
  73. ate/robot/agentic_servo.py +856 -0
  74. ate/robot/behaviors.py +493 -0
  75. ate/robot/ble_capture.py +1000 -0
  76. ate/robot/ble_enumerate.py +506 -0
  77. ate/robot/calibration.py +668 -0
  78. ate/robot/calibration_state.py +388 -0
  79. ate/robot/commands.py +3735 -0
  80. ate/robot/direction_calibration.py +554 -0
  81. ate/robot/discovery.py +441 -0
  82. ate/robot/introspection.py +330 -0
  83. ate/robot/llm_system_id.py +654 -0
  84. ate/robot/locomotion_calibration.py +508 -0
  85. ate/robot/manager.py +270 -0
  86. ate/robot/marker_generator.py +611 -0
  87. ate/robot/perception.py +502 -0
  88. ate/robot/primitives.py +614 -0
  89. ate/robot/profiles.py +281 -0
  90. ate/robot/registry.py +322 -0
  91. ate/robot/servo_mapper.py +1153 -0
  92. ate/robot/skill_upload.py +675 -0
  93. ate/robot/target_calibration.py +500 -0
  94. ate/robot/teach.py +515 -0
  95. ate/robot/types.py +242 -0
  96. ate/robot/visual_labeler.py +1048 -0
  97. ate/robot/visual_servo_loop.py +494 -0
  98. ate/robot/visual_servoing.py +570 -0
  99. ate/robot/visual_system_id.py +906 -0
  100. ate/transports/__init__.py +121 -0
  101. ate/transports/base.py +394 -0
  102. ate/transports/ble.py +405 -0
  103. ate/transports/hybrid.py +444 -0
  104. ate/transports/serial.py +345 -0
  105. ate/urdf/__init__.py +30 -0
  106. ate/urdf/capture.py +582 -0
  107. ate/urdf/cloud.py +491 -0
  108. ate/urdf/collision.py +271 -0
  109. ate/urdf/commands.py +708 -0
  110. ate/urdf/depth.py +360 -0
  111. ate/urdf/inertial.py +312 -0
  112. ate/urdf/kinematics.py +330 -0
  113. ate/urdf/lifting.py +415 -0
  114. ate/urdf/meshing.py +300 -0
  115. ate/urdf/models/__init__.py +110 -0
  116. ate/urdf/models/depth_anything.py +253 -0
  117. ate/urdf/models/sam2.py +324 -0
  118. ate/urdf/motion_analysis.py +396 -0
  119. ate/urdf/pipeline.py +468 -0
  120. ate/urdf/scale.py +256 -0
  121. ate/urdf/scan_session.py +411 -0
  122. ate/urdf/segmentation.py +299 -0
  123. ate/urdf/synthesis.py +319 -0
  124. ate/urdf/topology.py +336 -0
  125. ate/urdf/validation.py +371 -0
  126. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
  127. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  128. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  129. foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
  130. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  131. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,388 @@
1
+ """
2
+ Calibration State Machine with Enforced Prerequisites.
3
+
4
+ This module ensures calibration steps are completed in the correct order,
5
+ making it IMPOSSIBLE to skip critical steps like direction calibration.
6
+
7
+ The Problem This Solves:
8
+ -----------------------
9
+ Previously, a user could run:
10
+ ate robot calibrate start # Get servo ranges
11
+ ate robot behavior pickup # CRASH! Wrong direction!
12
+
13
+ The behavior would fail catastrophically because we knew WHICH servos
14
+ existed but not WHETHER positive commands moved TOWARD or AWAY from targets.
15
+
16
+ The Solution:
17
+ ------------
18
+ Each calibration stage must be completed before the next can begin.
19
+ Commands check prerequisites and refuse to run if not met.
20
+
21
+ Stages:
22
+ 1. DEVICE_DISCOVERED - Robot found on USB/network
23
+ 2. SERVO_RANGES_KNOWN - Min/max/neutral for each servo
24
+ 3. STRUCTURE_IDENTIFIED - Which servo controls which joint
25
+ 4. DIRECTION_CALIBRATED - For each servo: does + move toward target?
26
+ 5. VALIDATED - Pickup test passed
27
+
28
+ Usage:
29
+ from ate.robot.calibration_state import CalibrationState, check_prerequisite
30
+
31
+ # Check if we can run a behavior
32
+ state = CalibrationState.load("my_robot")
33
+ if not state.direction_calibrated:
34
+ print("Run 'ate robot calibrate direction' first")
35
+ sys.exit(1)
36
+
37
+ # Or use the decorator
38
+ @check_prerequisite("direction_calibrated")
39
+ def robot_behavior_command(...):
40
+ ...
41
+ """
42
+
43
+ import json
44
+ import os
45
+ from dataclasses import dataclass, field, asdict
46
+ from datetime import datetime
47
+ from pathlib import Path
48
+ from typing import Optional, Dict, List, Any
49
+ from functools import wraps
50
+ import sys
51
+
52
+
53
+ # State storage directory
54
+ STATE_DIR = Path.home() / ".ate" / "calibration_state"
55
+
56
+
57
+ @dataclass
58
+ class DirectionMapping:
59
+ """Records whether positive servo values move toward or away from target."""
60
+ servo_id: int
61
+ toward_direction: str # "positive" or "negative"
62
+ confidence: float # 0.0 to 1.0
63
+ delta_pixels: float # measured movement in pixels
64
+ calibrated_at: str # ISO timestamp
65
+
66
+ @classmethod
67
+ def from_dict(cls, d: dict) -> "DirectionMapping":
68
+ return cls(**d)
69
+
70
+
71
+ @dataclass
72
+ class CalibrationState:
73
+ """
74
+ Tracks calibration progress for a robot.
75
+
76
+ Enforces that each stage is completed before the next can begin.
77
+ Persists to disk so progress is not lost between sessions.
78
+ """
79
+
80
+ robot_name: str
81
+
82
+ # Stage 1: Device discovered
83
+ device_discovered: bool = False
84
+ serial_port: Optional[str] = None
85
+ camera_ip: Optional[str] = None
86
+ discovered_at: Optional[str] = None
87
+
88
+ # Stage 2: Servo ranges known
89
+ servo_ranges_known: bool = False
90
+ servo_count: int = 0
91
+ servo_ranges: Dict[int, Dict[str, int]] = field(default_factory=dict)
92
+ ranges_calibrated_at: Optional[str] = None
93
+
94
+ # Stage 3: Kinematic structure identified
95
+ structure_identified: bool = False
96
+ joint_mappings: Dict[int, str] = field(default_factory=dict) # servo_id → joint_name
97
+ structure_identified_at: Optional[str] = None
98
+
99
+ # Stage 4: Direction calibrated (THE CRITICAL MISSING STEP)
100
+ direction_calibrated: bool = False
101
+ direction_mappings: Dict[int, DirectionMapping] = field(default_factory=dict)
102
+ direction_calibrated_at: Optional[str] = None
103
+
104
+ # Stage 5: Validated via task execution
105
+ validated: bool = False
106
+ validation_task: Optional[str] = None
107
+ validated_at: Optional[str] = None
108
+
109
+ # Metadata
110
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
111
+ updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
112
+
113
+ def save(self) -> bool:
114
+ """Persist state to disk."""
115
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
116
+ path = STATE_DIR / f"{self.robot_name}.json"
117
+
118
+ self.updated_at = datetime.now().isoformat()
119
+
120
+ # Convert to JSON-serializable dict
121
+ data = asdict(self)
122
+
123
+ # Convert DirectionMapping objects to dicts
124
+ if self.direction_mappings:
125
+ data["direction_mappings"] = {
126
+ str(k): asdict(v) if isinstance(v, DirectionMapping) else v
127
+ for k, v in self.direction_mappings.items()
128
+ }
129
+
130
+ try:
131
+ with open(path, "w") as f:
132
+ json.dump(data, f, indent=2)
133
+ return True
134
+ except Exception as e:
135
+ print(f"Error saving calibration state: {e}", file=sys.stderr)
136
+ return False
137
+
138
+ @classmethod
139
+ def load(cls, robot_name: str) -> "CalibrationState":
140
+ """Load state from disk, or create new if not exists."""
141
+ path = STATE_DIR / f"{robot_name}.json"
142
+
143
+ if not path.exists():
144
+ return cls(robot_name=robot_name)
145
+
146
+ try:
147
+ with open(path) as f:
148
+ data = json.load(f)
149
+
150
+ # Convert direction_mappings back to DirectionMapping objects
151
+ if "direction_mappings" in data and data["direction_mappings"]:
152
+ data["direction_mappings"] = {
153
+ int(k): DirectionMapping.from_dict(v)
154
+ for k, v in data["direction_mappings"].items()
155
+ }
156
+
157
+ return cls(**data)
158
+ except Exception as e:
159
+ print(f"Error loading calibration state: {e}", file=sys.stderr)
160
+ return cls(robot_name=robot_name)
161
+
162
+ @classmethod
163
+ def list_all(cls) -> List[str]:
164
+ """List all saved calibration states."""
165
+ if not STATE_DIR.exists():
166
+ return []
167
+ return [p.stem for p in STATE_DIR.glob("*.json")]
168
+
169
+ def get_current_stage(self) -> str:
170
+ """Get the name of the current (incomplete) stage."""
171
+ if not self.device_discovered:
172
+ return "DEVICE_DISCOVERY"
173
+ if not self.servo_ranges_known:
174
+ return "SERVO_RANGE_DISCOVERY"
175
+ if not self.structure_identified:
176
+ return "STRUCTURE_IDENTIFICATION"
177
+ if not self.direction_calibrated:
178
+ return "DIRECTION_CALIBRATION"
179
+ if not self.validated:
180
+ return "VALIDATION"
181
+ return "COMPLETE"
182
+
183
+ def get_next_command(self) -> str:
184
+ """Get the command to run for the next stage."""
185
+ stage = self.get_current_stage()
186
+ commands = {
187
+ "DEVICE_DISCOVERY": "ate robot discover",
188
+ "SERVO_RANGE_DISCOVERY": "ate robot calibrate start",
189
+ "STRUCTURE_IDENTIFICATION": "ate robot calibrate structure",
190
+ "DIRECTION_CALIBRATION": "ate robot calibrate direction",
191
+ "VALIDATION": "ate robot calibrate validate",
192
+ "COMPLETE": "(calibration complete)",
193
+ }
194
+ return commands.get(stage, "unknown")
195
+
196
+ def check_prerequisite(self, required_stage: str) -> tuple[bool, str]:
197
+ """
198
+ Check if a prerequisite stage is complete.
199
+
200
+ Returns:
201
+ (passed, message) - passed is True if OK, message explains what's needed
202
+ """
203
+ stages = {
204
+ "device_discovered": (self.device_discovered, "ate robot discover"),
205
+ "servo_ranges_known": (self.servo_ranges_known, "ate robot calibrate start"),
206
+ "structure_identified": (self.structure_identified, "ate robot calibrate structure"),
207
+ "direction_calibrated": (self.direction_calibrated, "ate robot calibrate direction"),
208
+ "validated": (self.validated, "ate robot calibrate validate"),
209
+ }
210
+
211
+ if required_stage not in stages:
212
+ return True, ""
213
+
214
+ passed, command = stages[required_stage]
215
+
216
+ if passed:
217
+ return True, ""
218
+
219
+ # Build error message showing what's needed
220
+ msg = f"\n{'='*60}\n"
221
+ msg += "PREREQUISITE NOT MET\n"
222
+ msg += f"{'='*60}\n\n"
223
+ msg += f"Required: {required_stage.replace('_', ' ').title()}\n\n"
224
+ msg += "You must complete calibration steps in order:\n\n"
225
+
226
+ checklist = [
227
+ ("device_discovered", "Device Discovery", "ate robot discover"),
228
+ ("servo_ranges_known", "Servo Ranges", "ate robot calibrate start"),
229
+ ("structure_identified", "Kinematic Structure", "ate robot calibrate structure"),
230
+ ("direction_calibrated", "Direction Mapping", "ate robot calibrate direction"),
231
+ ("validated", "Task Validation", "ate robot calibrate validate"),
232
+ ]
233
+
234
+ for stage_key, name, cmd in checklist:
235
+ status = "✓" if getattr(self, stage_key) else "○"
236
+ arrow = " ← NEXT" if stage_key == required_stage else ""
237
+ msg += f" {status} {name}{arrow}\n"
238
+ if stage_key == required_stage:
239
+ break
240
+
241
+ msg += f"\nRun: {command}\n"
242
+
243
+ return False, msg
244
+
245
+ # Stage completion methods
246
+
247
+ def complete_device_discovery(
248
+ self,
249
+ serial_port: Optional[str] = None,
250
+ camera_ip: Optional[str] = None,
251
+ ) -> None:
252
+ """Mark device discovery as complete."""
253
+ self.device_discovered = True
254
+ self.serial_port = serial_port
255
+ self.camera_ip = camera_ip
256
+ self.discovered_at = datetime.now().isoformat()
257
+ self.save()
258
+
259
+ def complete_servo_ranges(
260
+ self,
261
+ servo_count: int,
262
+ servo_ranges: Dict[int, Dict[str, int]],
263
+ ) -> None:
264
+ """Mark servo range discovery as complete."""
265
+ self.servo_ranges_known = True
266
+ self.servo_count = servo_count
267
+ self.servo_ranges = servo_ranges
268
+ self.ranges_calibrated_at = datetime.now().isoformat()
269
+ self.save()
270
+
271
+ def complete_structure_identification(
272
+ self,
273
+ joint_mappings: Dict[int, str],
274
+ ) -> None:
275
+ """Mark structure identification as complete."""
276
+ self.structure_identified = True
277
+ self.joint_mappings = joint_mappings
278
+ self.structure_identified_at = datetime.now().isoformat()
279
+ self.save()
280
+
281
+ def complete_direction_calibration(
282
+ self,
283
+ direction_mappings: Dict[int, DirectionMapping],
284
+ ) -> None:
285
+ """Mark direction calibration as complete."""
286
+ self.direction_calibrated = True
287
+ self.direction_mappings = direction_mappings
288
+ self.direction_calibrated_at = datetime.now().isoformat()
289
+ self.save()
290
+
291
+ def complete_validation(self, task_name: str) -> None:
292
+ """Mark validation as complete."""
293
+ self.validated = True
294
+ self.validation_task = task_name
295
+ self.validated_at = datetime.now().isoformat()
296
+ self.save()
297
+
298
+ def reset(self) -> None:
299
+ """Reset all calibration state."""
300
+ self.__init__(robot_name=self.robot_name)
301
+ self.save()
302
+
303
+ def print_status(self) -> None:
304
+ """Print current calibration status."""
305
+ print(f"\nCalibration Status: {self.robot_name}")
306
+ print("=" * 50)
307
+
308
+ stages = [
309
+ ("device_discovered", "Device Discovery", self.discovered_at),
310
+ ("servo_ranges_known", "Servo Ranges", self.ranges_calibrated_at),
311
+ ("structure_identified", "Kinematic Structure", self.structure_identified_at),
312
+ ("direction_calibrated", "Direction Mapping", self.direction_calibrated_at),
313
+ ("validated", "Task Validation", self.validated_at),
314
+ ]
315
+
316
+ for key, name, timestamp in stages:
317
+ status = "✓" if getattr(self, key) else "○"
318
+ time_str = f" ({timestamp[:10]})" if timestamp else ""
319
+ print(f" {status} {name}{time_str}")
320
+
321
+ print()
322
+
323
+ current = self.get_current_stage()
324
+ if current != "COMPLETE":
325
+ print(f"Next step: {self.get_next_command()}")
326
+ else:
327
+ print("Robot is fully calibrated and validated!")
328
+
329
+
330
+ def check_prerequisite(required_stage: str):
331
+ """
332
+ Decorator that checks calibration prerequisites before running a command.
333
+
334
+ Usage:
335
+ @check_prerequisite("direction_calibrated")
336
+ def robot_behavior_command(profile_name: str, ...):
337
+ # Will only run if direction_calibrated is True
338
+ ...
339
+ """
340
+ def decorator(func):
341
+ @wraps(func)
342
+ def wrapper(*args, **kwargs):
343
+ # Try to get robot name from various sources
344
+ robot_name = None
345
+
346
+ # Check kwargs
347
+ for key in ["profile_name", "name", "robot_name"]:
348
+ if key in kwargs and kwargs[key]:
349
+ robot_name = kwargs[key]
350
+ break
351
+
352
+ # Check positional args (common patterns)
353
+ if not robot_name and args:
354
+ robot_name = args[0] if isinstance(args[0], str) else None
355
+
356
+ if not robot_name:
357
+ # Can't check without robot name - let the function handle it
358
+ return func(*args, **kwargs)
359
+
360
+ # Load state and check prerequisite
361
+ state = CalibrationState.load(robot_name)
362
+ passed, message = state.check_prerequisite(required_stage)
363
+
364
+ if not passed:
365
+ print(message, file=sys.stderr)
366
+ sys.exit(1)
367
+
368
+ return func(*args, **kwargs)
369
+
370
+ return wrapper
371
+ return decorator
372
+
373
+
374
+ def require_direction_calibration(robot_name: str) -> bool:
375
+ """
376
+ Check if direction calibration is complete. Exit if not.
377
+
378
+ Use this in commands that need direction calibration.
379
+ Returns True if calibrated, exits with error if not.
380
+ """
381
+ state = CalibrationState.load(robot_name)
382
+ passed, message = state.check_prerequisite("direction_calibrated")
383
+
384
+ if not passed:
385
+ print(message, file=sys.stderr)
386
+ return False
387
+
388
+ return True