foodforthought-cli 0.2.7__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.
- ate/behaviors/__init__.py +88 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +610 -54
- ate/drivers/__init__.py +27 -0
- ate/drivers/mechdog.py +606 -0
- ate/interfaces/__init__.py +171 -0
- ate/interfaces/base.py +271 -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/types.py +371 -0
- ate/mcp_server.py +387 -0
- ate/recording/__init__.py +44 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +405 -0
- ate/recording/upload.py +304 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +79 -0
- ate/robot/calibration.py +583 -0
- ate/robot/commands.py +3603 -0
- ate/robot/discovery.py +339 -0
- ate/robot/introspection.py +330 -0
- ate/robot/manager.py +270 -0
- ate/robot/profiles.py +275 -0
- ate/robot/registry.py +319 -0
- ate/robot/skill_upload.py +393 -0
- ate/robot/visual_labeler.py +1039 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +36 -7
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.7.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())
|