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.
- 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 +100 -0
- ate/behaviors/approach.py +399 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +855 -3995
- 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 +39 -0
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +942 -0
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +187 -0
- ate/interfaces/base.py +273 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/sensors.py +247 -0
- ate/interfaces/types.py +371 -0
- ate/llm_proxy.py +239 -0
- ate/mcp_server.py +387 -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 +83 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +415 -0
- ate/recording/upload.py +304 -0
- ate/recording/visual.py +416 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +221 -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 +668 -0
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +3735 -0
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +441 -0
- ate/robot/introspection.py +330 -0
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/manager.py +270 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +281 -0
- ate/robot/registry.py +322 -0
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +675 -0
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +1048 -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.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {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
|