foodforthought-cli 0.2.4__py3-none-any.whl → 0.2.8__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 (37) hide show
  1. ate/__init__.py +1 -1
  2. ate/behaviors/__init__.py +88 -0
  3. ate/behaviors/common.py +686 -0
  4. ate/behaviors/tree.py +454 -0
  5. ate/cli.py +610 -54
  6. ate/drivers/__init__.py +27 -0
  7. ate/drivers/mechdog.py +606 -0
  8. ate/interfaces/__init__.py +171 -0
  9. ate/interfaces/base.py +271 -0
  10. ate/interfaces/body.py +267 -0
  11. ate/interfaces/detection.py +282 -0
  12. ate/interfaces/locomotion.py +422 -0
  13. ate/interfaces/manipulation.py +408 -0
  14. ate/interfaces/navigation.py +389 -0
  15. ate/interfaces/perception.py +362 -0
  16. ate/interfaces/types.py +371 -0
  17. ate/mcp_server.py +387 -0
  18. ate/recording/__init__.py +44 -0
  19. ate/recording/demonstration.py +378 -0
  20. ate/recording/session.py +405 -0
  21. ate/recording/upload.py +304 -0
  22. ate/recording/wrapper.py +95 -0
  23. ate/robot/__init__.py +79 -0
  24. ate/robot/calibration.py +583 -0
  25. ate/robot/commands.py +3603 -0
  26. ate/robot/discovery.py +339 -0
  27. ate/robot/introspection.py +330 -0
  28. ate/robot/manager.py +270 -0
  29. ate/robot/profiles.py +275 -0
  30. ate/robot/registry.py +319 -0
  31. ate/robot/skill_upload.py +393 -0
  32. ate/robot/visual_labeler.py +1039 -0
  33. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
  34. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +37 -8
  35. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
  36. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
  37. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/top_level.txt +0 -0
ate/robot/manager.py ADDED
@@ -0,0 +1,270 @@
1
+ """
2
+ Robot Manager - unified interface for managing robot connections.
3
+
4
+ Handles:
5
+ - Loading robots from profiles
6
+ - Connecting/disconnecting
7
+ - Managing multiple robots
8
+ """
9
+
10
+ from typing import Dict, List, Optional, Any
11
+ from dataclasses import dataclass
12
+
13
+ from .profiles import RobotProfile, load_profile, list_profiles
14
+ from .registry import KNOWN_ROBOTS, RobotType
15
+ from .introspection import get_capabilities, get_methods
16
+
17
+
18
+ @dataclass
19
+ class ManagedRobot:
20
+ """A robot instance managed by RobotManager."""
21
+ profile: RobotProfile
22
+ robot_type: RobotType
23
+ instance: Any = None
24
+ connected: bool = False
25
+
26
+
27
+ class RobotManager:
28
+ """
29
+ Manages robot connections and lifecycle.
30
+
31
+ Example:
32
+ manager = RobotManager()
33
+
34
+ # Load from profile
35
+ dog = manager.load("my_mechdog")
36
+ dog.connect()
37
+
38
+ # Use robot
39
+ dog.instance.stand()
40
+ dog.instance.walk(Vector3.forward())
41
+
42
+ # Disconnect
43
+ manager.disconnect_all()
44
+ """
45
+
46
+ def __init__(self):
47
+ self._robots: Dict[str, ManagedRobot] = {}
48
+
49
+ def load(self, profile_name: str) -> Optional[ManagedRobot]:
50
+ """
51
+ Load a robot from a saved profile.
52
+
53
+ Args:
54
+ profile_name: Name of the profile
55
+
56
+ Returns:
57
+ ManagedRobot or None if profile not found
58
+ """
59
+ profile = load_profile(profile_name)
60
+ if profile is None:
61
+ return None
62
+
63
+ robot_type = KNOWN_ROBOTS.get(profile.robot_type)
64
+ if robot_type is None:
65
+ print(f"Unknown robot type: {profile.robot_type}")
66
+ return None
67
+
68
+ managed = ManagedRobot(
69
+ profile=profile,
70
+ robot_type=robot_type,
71
+ )
72
+
73
+ self._robots[profile_name] = managed
74
+ return managed
75
+
76
+ def create(
77
+ self,
78
+ name: str,
79
+ robot_type: str,
80
+ **config
81
+ ) -> Optional[ManagedRobot]:
82
+ """
83
+ Create a robot instance without loading from profile.
84
+
85
+ Args:
86
+ name: Name for this robot
87
+ robot_type: Robot type ID
88
+ **config: Configuration options
89
+
90
+ Returns:
91
+ ManagedRobot
92
+ """
93
+ rtype = KNOWN_ROBOTS.get(robot_type)
94
+ if rtype is None:
95
+ print(f"Unknown robot type: {robot_type}")
96
+ return None
97
+
98
+ profile = RobotProfile(
99
+ name=name,
100
+ robot_type=robot_type,
101
+ **config
102
+ )
103
+
104
+ managed = ManagedRobot(
105
+ profile=profile,
106
+ robot_type=rtype,
107
+ )
108
+
109
+ self._robots[name] = managed
110
+ return managed
111
+
112
+ def connect(self, name: str) -> bool:
113
+ """
114
+ Connect to a loaded robot.
115
+
116
+ Args:
117
+ name: Robot name
118
+
119
+ Returns:
120
+ True if connected successfully
121
+ """
122
+ managed = self._robots.get(name)
123
+ if managed is None:
124
+ print(f"Robot not loaded: {name}")
125
+ return False
126
+
127
+ # Create robot instance
128
+ instance = self._create_instance(managed)
129
+ if instance is None:
130
+ return False
131
+
132
+ managed.instance = instance
133
+
134
+ # Connect
135
+ result = instance.connect()
136
+ if result.success:
137
+ managed.connected = True
138
+ return True
139
+ else:
140
+ print(f"Connection failed: {result.message}")
141
+ return False
142
+
143
+ def disconnect(self, name: str) -> bool:
144
+ """
145
+ Disconnect from a robot.
146
+
147
+ Args:
148
+ name: Robot name
149
+
150
+ Returns:
151
+ True if disconnected
152
+ """
153
+ managed = self._robots.get(name)
154
+ if managed is None or not managed.connected:
155
+ return True
156
+
157
+ if managed.instance:
158
+ managed.instance.disconnect()
159
+ managed.connected = False
160
+
161
+ return True
162
+
163
+ def disconnect_all(self) -> None:
164
+ """Disconnect from all robots."""
165
+ for name in list(self._robots.keys()):
166
+ self.disconnect(name)
167
+
168
+ def get(self, name: str) -> Optional[Any]:
169
+ """
170
+ Get a robot instance.
171
+
172
+ Args:
173
+ name: Robot name
174
+
175
+ Returns:
176
+ Robot instance or None
177
+ """
178
+ managed = self._robots.get(name)
179
+ if managed and managed.instance:
180
+ return managed.instance
181
+ return None
182
+
183
+ def list_loaded(self) -> List[str]:
184
+ """List all loaded robots."""
185
+ return list(self._robots.keys())
186
+
187
+ def list_connected(self) -> List[str]:
188
+ """List all connected robots."""
189
+ return [
190
+ name for name, managed in self._robots.items()
191
+ if managed.connected
192
+ ]
193
+
194
+ def get_info(self, name: str) -> Optional[Dict[str, Any]]:
195
+ """
196
+ Get information about a loaded robot.
197
+
198
+ Args:
199
+ name: Robot name
200
+
201
+ Returns:
202
+ Dict with robot info
203
+ """
204
+ managed = self._robots.get(name)
205
+ if managed is None:
206
+ return None
207
+
208
+ info = {
209
+ "name": managed.profile.name,
210
+ "type": managed.robot_type.name,
211
+ "manufacturer": managed.robot_type.manufacturer,
212
+ "archetype": managed.robot_type.archetype,
213
+ "connected": managed.connected,
214
+ "profile": {
215
+ "serial_port": managed.profile.serial_port,
216
+ "camera_ip": managed.profile.camera_ip,
217
+ "has_camera": managed.profile.has_camera,
218
+ "has_arm": managed.profile.has_arm,
219
+ },
220
+ }
221
+
222
+ if managed.instance:
223
+ info["capabilities"] = list(get_capabilities(managed.instance).keys())
224
+ info["methods"] = {
225
+ cap: [m.name for m in methods]
226
+ for cap, methods in get_methods(managed.instance).items()
227
+ }
228
+
229
+ return info
230
+
231
+ def _create_instance(self, managed: ManagedRobot) -> Optional[Any]:
232
+ """Create a robot instance from profile and type."""
233
+ rtype = managed.robot_type
234
+ profile = managed.profile
235
+
236
+ # Import the driver module dynamically
237
+ try:
238
+ import importlib
239
+ module = importlib.import_module(rtype.driver_module)
240
+ driver_class = getattr(module, rtype.driver_class)
241
+ config_class = getattr(module, rtype.config_class, None)
242
+ except (ImportError, AttributeError) as e:
243
+ print(f"Failed to import driver: {e}")
244
+ return None
245
+
246
+ # Build configuration
247
+ if config_class:
248
+ config_kwargs = {
249
+ "port": profile.serial_port or "/dev/ttyUSB0",
250
+ }
251
+
252
+ if profile.has_camera and profile.camera_ip:
253
+ config_kwargs["has_camera"] = True
254
+ config_kwargs["camera_ip"] = profile.camera_ip
255
+ config_kwargs["camera_port"] = profile.camera_port
256
+ config_kwargs["camera_stream_port"] = profile.camera_stream_port
257
+
258
+ if profile.has_arm:
259
+ config_kwargs["has_arm"] = True
260
+
261
+ config = config_class(**config_kwargs)
262
+ return driver_class(config=config)
263
+ else:
264
+ return driver_class()
265
+
266
+ def __enter__(self) -> "RobotManager":
267
+ return self
268
+
269
+ def __exit__(self, *args) -> None:
270
+ self.disconnect_all()
ate/robot/profiles.py ADDED
@@ -0,0 +1,275 @@
1
+ """
2
+ Robot profiles - saved configurations for easy robot setup.
3
+
4
+ Profiles are stored in ~/.ate/robots/ as JSON files.
5
+ Users can create, edit, and share profiles.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ from dataclasses import dataclass, field, asdict
11
+ from typing import Dict, List, Optional, Any
12
+ from pathlib import Path
13
+
14
+
15
+ # Default profile directory
16
+ def get_profiles_dir() -> Path:
17
+ """Get the profiles directory, creating if needed."""
18
+ ate_dir = Path.home() / ".ate"
19
+ profiles_dir = ate_dir / "robots"
20
+ profiles_dir.mkdir(parents=True, exist_ok=True)
21
+ return profiles_dir
22
+
23
+
24
+ @dataclass
25
+ class RobotProfile:
26
+ """
27
+ A saved robot configuration.
28
+
29
+ Contains everything needed to connect to and use a robot.
30
+ """
31
+ # Identity
32
+ name: str # User-chosen name for this robot
33
+ robot_type: str # ID of robot type from registry
34
+
35
+ # Serial connection
36
+ serial_port: Optional[str] = None
37
+ baud_rate: int = 115200
38
+
39
+ # Network connection
40
+ ip_address: Optional[str] = None
41
+ camera_ip: Optional[str] = None
42
+ camera_port: int = 80
43
+ camera_stream_port: int = 81
44
+
45
+ # Robot-specific config
46
+ has_arm: bool = False
47
+ has_camera: bool = False
48
+ has_gripper: bool = False
49
+
50
+ # Custom settings
51
+ settings: Dict[str, Any] = field(default_factory=dict)
52
+
53
+ # Metadata
54
+ description: str = ""
55
+ created_at: Optional[str] = None
56
+ updated_at: Optional[str] = None
57
+
58
+ def to_dict(self) -> Dict[str, Any]:
59
+ """Convert to dictionary for YAML serialization."""
60
+ return asdict(self)
61
+
62
+ @classmethod
63
+ def from_dict(cls, data: Dict[str, Any]) -> "RobotProfile":
64
+ """Create from dictionary."""
65
+ return cls(**data)
66
+
67
+
68
+ def load_profile(name: str) -> Optional[RobotProfile]:
69
+ """
70
+ Load a robot profile by name.
71
+
72
+ Args:
73
+ name: Profile name (without .json extension)
74
+
75
+ Returns:
76
+ RobotProfile or None if not found
77
+ """
78
+ profiles_dir = get_profiles_dir()
79
+ profile_path = profiles_dir / f"{name}.json"
80
+
81
+ if not profile_path.exists():
82
+ return None
83
+
84
+ try:
85
+ with open(profile_path, "r") as f:
86
+ data = json.load(f)
87
+ return RobotProfile.from_dict(data)
88
+ except Exception as e:
89
+ print(f"Error loading profile: {e}")
90
+ return None
91
+
92
+
93
+ def save_profile(profile: RobotProfile) -> bool:
94
+ """
95
+ Save a robot profile.
96
+
97
+ Args:
98
+ profile: Profile to save
99
+
100
+ Returns:
101
+ True if saved successfully
102
+ """
103
+ from datetime import datetime
104
+
105
+ profiles_dir = get_profiles_dir()
106
+ profile_path = profiles_dir / f"{profile.name}.json"
107
+
108
+ # Update timestamps
109
+ now = datetime.now().isoformat()
110
+ if profile.created_at is None:
111
+ profile.created_at = now
112
+ profile.updated_at = now
113
+
114
+ try:
115
+ with open(profile_path, "w") as f:
116
+ json.dump(profile.to_dict(), f, indent=2)
117
+ return True
118
+ except Exception as e:
119
+ print(f"Error saving profile: {e}")
120
+ return False
121
+
122
+
123
+ def list_profiles() -> List[RobotProfile]:
124
+ """
125
+ List all saved robot profiles.
126
+
127
+ Returns:
128
+ List of profiles
129
+ """
130
+ profiles_dir = get_profiles_dir()
131
+ profiles = []
132
+
133
+ for file in profiles_dir.glob("*.json"):
134
+ try:
135
+ with open(file, "r") as f:
136
+ data = json.load(f)
137
+ profiles.append(RobotProfile.from_dict(data))
138
+ except Exception:
139
+ pass
140
+
141
+ return profiles
142
+
143
+
144
+ def delete_profile(name: str) -> bool:
145
+ """
146
+ Delete a robot profile.
147
+
148
+ Args:
149
+ name: Profile name
150
+
151
+ Returns:
152
+ True if deleted
153
+ """
154
+ profiles_dir = get_profiles_dir()
155
+ profile_path = profiles_dir / f"{name}.json"
156
+
157
+ if profile_path.exists():
158
+ profile_path.unlink()
159
+ return True
160
+ return False
161
+
162
+
163
+ def profile_exists(name: str) -> bool:
164
+ """Check if a profile exists."""
165
+ profiles_dir = get_profiles_dir()
166
+ return (profiles_dir / f"{name}.json").exists()
167
+
168
+
169
+ def get_default_profile() -> Optional[RobotProfile]:
170
+ """
171
+ Get the default profile (if set).
172
+
173
+ The default profile is stored as 'default.yaml' or
174
+ a profile named 'default'.
175
+ """
176
+ profiles_dir = get_profiles_dir()
177
+
178
+ # Check for default marker
179
+ default_marker = profiles_dir / ".default"
180
+ if default_marker.exists():
181
+ with open(default_marker, "r") as f:
182
+ default_name = f.read().strip()
183
+ return load_profile(default_name)
184
+
185
+ # Fall back to profile named 'default'
186
+ return load_profile("default")
187
+
188
+
189
+ def set_default_profile(name: str) -> bool:
190
+ """
191
+ Set a profile as the default.
192
+
193
+ Args:
194
+ name: Profile name
195
+
196
+ Returns:
197
+ True if set successfully
198
+ """
199
+ if not profile_exists(name):
200
+ return False
201
+
202
+ profiles_dir = get_profiles_dir()
203
+ default_marker = profiles_dir / ".default"
204
+
205
+ with open(default_marker, "w") as f:
206
+ f.write(name)
207
+
208
+ return True
209
+
210
+
211
+ def create_profile_from_discovery(
212
+ name: str,
213
+ serial_port: Optional[str] = None,
214
+ camera_ip: Optional[str] = None,
215
+ robot_type: str = "hiwonder_mechdog",
216
+ ) -> RobotProfile:
217
+ """
218
+ Create a profile from discovered devices.
219
+
220
+ Helper for the setup wizard.
221
+ """
222
+ profile = RobotProfile(
223
+ name=name,
224
+ robot_type=robot_type,
225
+ serial_port=serial_port,
226
+ camera_ip=camera_ip,
227
+ has_camera=camera_ip is not None,
228
+ )
229
+
230
+ return profile
231
+
232
+
233
+ # Built-in profile templates
234
+ PROFILE_TEMPLATES: Dict[str, RobotProfile] = {
235
+ "mechdog_basic": RobotProfile(
236
+ name="mechdog_basic",
237
+ robot_type="hiwonder_mechdog",
238
+ description="Basic MechDog setup without camera or arm",
239
+ serial_port="/dev/cu.usbserial-10", # Common macOS port
240
+ has_camera=False,
241
+ has_arm=False,
242
+ ),
243
+ "mechdog_camera": RobotProfile(
244
+ name="mechdog_camera",
245
+ robot_type="hiwonder_mechdog",
246
+ description="MechDog with visual module (camera)",
247
+ serial_port="/dev/cu.usbserial-10",
248
+ has_camera=True,
249
+ camera_ip="192.168.1.100",
250
+ ),
251
+ "mechdog_full": RobotProfile(
252
+ name="mechdog_full",
253
+ robot_type="hiwonder_mechdog",
254
+ description="Full MechDog setup with arm and camera",
255
+ serial_port="/dev/cu.usbserial-10",
256
+ has_camera=True,
257
+ has_arm=True,
258
+ camera_ip="192.168.1.100",
259
+ ),
260
+ "simulation": RobotProfile(
261
+ name="simulation",
262
+ robot_type="simulation",
263
+ description="Simulated robot for testing without hardware",
264
+ ),
265
+ }
266
+
267
+
268
+ def get_template(template_name: str) -> Optional[RobotProfile]:
269
+ """Get a profile template."""
270
+ return PROFILE_TEMPLATES.get(template_name)
271
+
272
+
273
+ def list_templates() -> List[str]:
274
+ """List available profile templates."""
275
+ return list(PROFILE_TEMPLATES.keys())