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,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
|
ate/robot/commands.py
CHANGED
|
@@ -1742,6 +1742,7 @@ def robot_upload_command(
|
|
|
1742
1742
|
name: str,
|
|
1743
1743
|
project_id: Optional[str] = None,
|
|
1744
1744
|
include_images: bool = True,
|
|
1745
|
+
dry_run: bool = False,
|
|
1745
1746
|
):
|
|
1746
1747
|
"""
|
|
1747
1748
|
Upload skill library to FoodforThought.
|
|
@@ -1752,6 +1753,7 @@ def robot_upload_command(
|
|
|
1752
1753
|
ate robot upload mechdog
|
|
1753
1754
|
ate robot upload mechdog --project-id abc123
|
|
1754
1755
|
ate robot upload mechdog --no-images
|
|
1756
|
+
ate robot upload mechdog --dry-run
|
|
1755
1757
|
"""
|
|
1756
1758
|
# Check login status
|
|
1757
1759
|
config_file = Path.home() / ".ate" / "config.json"
|
|
@@ -1775,16 +1777,67 @@ def robot_upload_command(
|
|
|
1775
1777
|
print("Run 'ate robot label' first to create skills.")
|
|
1776
1778
|
sys.exit(1)
|
|
1777
1779
|
|
|
1780
|
+
# Count images
|
|
1781
|
+
img_count = 0
|
|
1782
|
+
if include_images:
|
|
1783
|
+
img_dir = Path.home() / ".ate" / "skill_images" / name
|
|
1784
|
+
if img_dir.exists():
|
|
1785
|
+
img_count = len(list(img_dir.glob("*.jpg")))
|
|
1786
|
+
|
|
1787
|
+
# Dry run preview
|
|
1788
|
+
if dry_run:
|
|
1789
|
+
print("=" * 60)
|
|
1790
|
+
print("DRY RUN - No changes will be made")
|
|
1791
|
+
print("=" * 60)
|
|
1792
|
+
print()
|
|
1793
|
+
print(f"Would upload skill library: {name}")
|
|
1794
|
+
print(f" Robot model: {lib.robot_model}")
|
|
1795
|
+
print(f" Target project: {project_id or '(new project)'}")
|
|
1796
|
+
print()
|
|
1797
|
+
print("Artifacts that would be created:")
|
|
1798
|
+
print()
|
|
1799
|
+
|
|
1800
|
+
# Raw artifacts (images)
|
|
1801
|
+
if include_images and img_count > 0:
|
|
1802
|
+
print(f" [raw] {img_count} pose images")
|
|
1803
|
+
for img_path in list(img_dir.glob("*.jpg"))[:3]:
|
|
1804
|
+
print(f" - {img_path.name}")
|
|
1805
|
+
if img_count > 3:
|
|
1806
|
+
print(f" ... and {img_count - 3} more")
|
|
1807
|
+
|
|
1808
|
+
# Processed artifact (calibration)
|
|
1809
|
+
print(f" [processed] {name}_calibration")
|
|
1810
|
+
print(f" - {len(cal.servos)} servos configured")
|
|
1811
|
+
|
|
1812
|
+
# Labeled artifacts (poses)
|
|
1813
|
+
print(f" [labeled] {len(cal.poses)} poses:")
|
|
1814
|
+
for pose_name in list(cal.poses.keys())[:5]:
|
|
1815
|
+
print(f" - {pose_name}")
|
|
1816
|
+
if len(cal.poses) > 5:
|
|
1817
|
+
print(f" ... and {len(cal.poses) - 5} more")
|
|
1818
|
+
|
|
1819
|
+
# Skill artifacts
|
|
1820
|
+
print(f" [skill] {len(lib.actions)} skills:")
|
|
1821
|
+
for action_name in list(lib.actions.keys())[:5]:
|
|
1822
|
+
print(f" - {action_name}")
|
|
1823
|
+
if len(lib.actions) > 5:
|
|
1824
|
+
print(f" ... and {len(lib.actions) - 5} more")
|
|
1825
|
+
|
|
1826
|
+
print()
|
|
1827
|
+
print("Total artifacts: ", end="")
|
|
1828
|
+
total = img_count + 1 + len(cal.poses) + len(lib.actions)
|
|
1829
|
+
print(f"{total}")
|
|
1830
|
+
print()
|
|
1831
|
+
print("Run without --dry-run to perform the upload.")
|
|
1832
|
+
return
|
|
1833
|
+
|
|
1778
1834
|
print(f"Uploading skill library: {name}")
|
|
1779
1835
|
print(f" Robot model: {lib.robot_model}")
|
|
1780
1836
|
print(f" Actions: {len(lib.actions)}")
|
|
1781
1837
|
print(f" Poses: {len(cal.poses)}")
|
|
1782
1838
|
print(f" Servos: {len(cal.servos)}")
|
|
1783
|
-
if include_images:
|
|
1784
|
-
|
|
1785
|
-
if img_dir.exists():
|
|
1786
|
-
img_count = len(list(img_dir.glob("*.jpg")))
|
|
1787
|
-
print(f" Images: {img_count}")
|
|
1839
|
+
if include_images and img_count > 0:
|
|
1840
|
+
print(f" Images: {img_count}")
|
|
1788
1841
|
print()
|
|
1789
1842
|
|
|
1790
1843
|
try:
|
|
@@ -1894,12 +1947,14 @@ def robot_upload_calibration_command(
|
|
|
1894
1947
|
robot_name: Optional[str] = None,
|
|
1895
1948
|
method: str = "llm_vision",
|
|
1896
1949
|
publish: bool = False,
|
|
1950
|
+
dry_run: bool = False,
|
|
1897
1951
|
):
|
|
1898
1952
|
"""
|
|
1899
1953
|
Upload a calibration to the community registry.
|
|
1900
1954
|
|
|
1901
1955
|
ate robot upload-calibration ./servo_mappings.json --robot-slug mechdog-mini
|
|
1902
1956
|
ate robot upload-calibration ./calibration.json --publish
|
|
1957
|
+
ate robot upload-calibration ./calibration.json --dry-run
|
|
1903
1958
|
"""
|
|
1904
1959
|
import json
|
|
1905
1960
|
import requests
|
|
@@ -1913,6 +1968,38 @@ def robot_upload_calibration_command(
|
|
|
1913
1968
|
with open(calibration_file, 'r') as f:
|
|
1914
1969
|
calibration_data = json.load(f)
|
|
1915
1970
|
|
|
1971
|
+
# Dry run preview
|
|
1972
|
+
if dry_run:
|
|
1973
|
+
print("=" * 60)
|
|
1974
|
+
print("DRY RUN - No changes will be made")
|
|
1975
|
+
print("=" * 60)
|
|
1976
|
+
print()
|
|
1977
|
+
print(f"Would upload calibration from: {calibration_file}")
|
|
1978
|
+
print()
|
|
1979
|
+
print("Calibration details:")
|
|
1980
|
+
print(f" Name: {calibration_data.get('name', f'Calibration from {calibration_file}')}")
|
|
1981
|
+
print(f" Version: {calibration_data.get('version', '1.0.0')}")
|
|
1982
|
+
print(f" Method: {method}")
|
|
1983
|
+
print(f" Robot: {robot_slug or calibration_data.get('robot_slug', 'Not specified')}")
|
|
1984
|
+
print()
|
|
1985
|
+
|
|
1986
|
+
# Show what data would be uploaded
|
|
1987
|
+
if calibration_data.get("servos"):
|
|
1988
|
+
print(f" Servos: {len(calibration_data['servos'])}")
|
|
1989
|
+
if calibration_data.get("joint_mappings") or calibration_data.get("mappings"):
|
|
1990
|
+
mappings = calibration_data.get("joint_mappings") or calibration_data.get("mappings")
|
|
1991
|
+
print(f" Joint mappings: {len(mappings) if isinstance(mappings, (list, dict)) else 'present'}")
|
|
1992
|
+
if calibration_data.get("urdf"):
|
|
1993
|
+
print(f" URDF: included ({len(calibration_data['urdf'])} chars)")
|
|
1994
|
+
if calibration_data.get("python_code"):
|
|
1995
|
+
print(f" Python code: included ({len(calibration_data['python_code'])} chars)")
|
|
1996
|
+
|
|
1997
|
+
print()
|
|
1998
|
+
print(f" Publish immediately: {publish}")
|
|
1999
|
+
print()
|
|
2000
|
+
print("Run without --dry-run to perform the upload.")
|
|
2001
|
+
return
|
|
2002
|
+
|
|
1916
2003
|
# Get API credentials
|
|
1917
2004
|
try:
|
|
1918
2005
|
uploader = SkillLibraryUploader()
|
|
@@ -1975,15 +2062,54 @@ def robot_upload_calibration_command(
|
|
|
1975
2062
|
json=payload,
|
|
1976
2063
|
headers=headers,
|
|
1977
2064
|
)
|
|
1978
|
-
|
|
2065
|
+
|
|
2066
|
+
if not response.ok:
|
|
2067
|
+
# Parse error and provide helpful suggestions
|
|
2068
|
+
try:
|
|
2069
|
+
error_body = response.json()
|
|
2070
|
+
except:
|
|
2071
|
+
error_body = {"raw": response.text}
|
|
2072
|
+
|
|
2073
|
+
print(f"Error uploading calibration: {response.status_code} {response.reason}")
|
|
2074
|
+
|
|
2075
|
+
error_msg = error_body.get("error") or error_body.get("message", "")
|
|
2076
|
+
if error_msg:
|
|
2077
|
+
print(f"Details: {error_msg}")
|
|
2078
|
+
|
|
2079
|
+
# Provide specific suggestions based on error
|
|
2080
|
+
print("\nSuggestions:")
|
|
2081
|
+
if response.status_code == 404 and "robot" in str(error_body).lower():
|
|
2082
|
+
print(" - Robot slug may not exist in the database")
|
|
2083
|
+
print(" - Search for robots: ate robot identify --search <partial-name>")
|
|
2084
|
+
print(" - Try without --robot-slug to search by robot name")
|
|
2085
|
+
# Try to find similar robots
|
|
2086
|
+
if robot_slug:
|
|
2087
|
+
print(f"\n Looking for robots similar to '{robot_slug}'...")
|
|
2088
|
+
try:
|
|
2089
|
+
search_resp = requests.get(
|
|
2090
|
+
f"{base_url}/api/robots?search={robot_slug.replace('-', ' ')}",
|
|
2091
|
+
headers=headers,
|
|
2092
|
+
)
|
|
2093
|
+
if search_resp.ok:
|
|
2094
|
+
robots = search_resp.json().get("robots", [])
|
|
2095
|
+
if robots:
|
|
2096
|
+
print(" Found similar robots:")
|
|
2097
|
+
for r in robots[:5]:
|
|
2098
|
+
print(f" - {r.get('slug')}: {r.get('name')}")
|
|
2099
|
+
except:
|
|
2100
|
+
pass
|
|
2101
|
+
|
|
2102
|
+
elif response.status_code == 400:
|
|
2103
|
+
print(" - Check calibration file contains required fields")
|
|
2104
|
+
print(" - Ensure robot_slug or robot_id is specified")
|
|
2105
|
+
elif response.status_code == 401:
|
|
2106
|
+
print(" - Session expired. Run: ate login")
|
|
2107
|
+
|
|
2108
|
+
sys.exit(1)
|
|
2109
|
+
|
|
1979
2110
|
result = response.json()
|
|
1980
2111
|
except requests.RequestException as e:
|
|
1981
2112
|
print(f"Error uploading calibration: {e}")
|
|
1982
|
-
if hasattr(e.response, 'json'):
|
|
1983
|
-
try:
|
|
1984
|
-
print(f"Details: {e.response.json()}")
|
|
1985
|
-
except:
|
|
1986
|
-
pass
|
|
1987
2113
|
sys.exit(1)
|
|
1988
2114
|
|
|
1989
2115
|
print("[OK] Calibration uploaded successfully!")
|
|
@@ -3095,6 +3221,8 @@ EXAMPLES:
|
|
|
3095
3221
|
upload_parser.add_argument("--project-id", dest="project_id", help="Existing project ID (creates new if omitted)")
|
|
3096
3222
|
upload_parser.add_argument("--no-images", action="store_true", dest="no_images",
|
|
3097
3223
|
help="Skip uploading pose images")
|
|
3224
|
+
upload_parser.add_argument("--dry-run", action="store_true",
|
|
3225
|
+
help="Preview what would be uploaded without making changes")
|
|
3098
3226
|
|
|
3099
3227
|
# robot teach - Fast keyboard-driven teaching mode
|
|
3100
3228
|
teach_parser = robot_subparsers.add_parser("teach",
|
|
@@ -3257,6 +3385,8 @@ EXAMPLES:
|
|
|
3257
3385
|
choices=["aruco_marker", "llm_vision", "manual", "urdf_import", "twitch_test", "direction_calibration"],
|
|
3258
3386
|
default="llm_vision",
|
|
3259
3387
|
help="Calibration method (default: llm_vision)")
|
|
3388
|
+
upload_cal_parser.add_argument("--dry-run", action="store_true",
|
|
3389
|
+
help="Preview what would be uploaded without making changes")
|
|
3260
3390
|
upload_cal_parser.add_argument("--publish", action="store_true",
|
|
3261
3391
|
help="Publish immediately (default: save as draft)")
|
|
3262
3392
|
|
|
@@ -3501,6 +3631,7 @@ def handle_robot_command(args):
|
|
|
3501
3631
|
name=args.name,
|
|
3502
3632
|
project_id=getattr(args, "project_id", None),
|
|
3503
3633
|
include_images=not getattr(args, "no_images", False),
|
|
3634
|
+
dry_run=getattr(args, "dry_run", False),
|
|
3504
3635
|
)
|
|
3505
3636
|
|
|
3506
3637
|
elif args.robot_action == "teach":
|
|
@@ -3563,6 +3694,7 @@ def handle_robot_command(args):
|
|
|
3563
3694
|
robot_name=getattr(args, "robot_name", None),
|
|
3564
3695
|
method=getattr(args, "method", "llm_vision"),
|
|
3565
3696
|
publish=getattr(args, "publish", False),
|
|
3697
|
+
dry_run=getattr(args, "dry_run", False),
|
|
3566
3698
|
)
|
|
3567
3699
|
|
|
3568
3700
|
elif args.robot_action == "urdf":
|