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/discovery.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-discovery of robots on network and USB.
|
|
3
|
+
|
|
4
|
+
Scans for:
|
|
5
|
+
- Serial devices matching known patterns
|
|
6
|
+
- Network cameras (ESP32-CAM, IP cameras)
|
|
7
|
+
- ROS2 topics (if ROS2 available)
|
|
8
|
+
- mDNS services
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import glob
|
|
13
|
+
import time
|
|
14
|
+
import platform
|
|
15
|
+
import concurrent.futures
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import List, Optional, Dict, Any
|
|
18
|
+
from enum import Enum, auto
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import serial
|
|
22
|
+
import serial.tools.list_ports
|
|
23
|
+
HAS_SERIAL = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
HAS_SERIAL = False
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import requests
|
|
29
|
+
HAS_REQUESTS = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
HAS_REQUESTS = False
|
|
32
|
+
|
|
33
|
+
from .registry import KNOWN_ROBOTS, RobotType, ConnectionType
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DiscoveryStatus(Enum):
|
|
37
|
+
"""Status of discovered device."""
|
|
38
|
+
FOUND = auto() # Device found
|
|
39
|
+
IDENTIFIED = auto() # Device identified as known robot
|
|
40
|
+
CONNECTED = auto() # Successfully connected
|
|
41
|
+
ERROR = auto() # Error during discovery
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DiscoveredRobot:
|
|
46
|
+
"""A robot discovered on the network or USB."""
|
|
47
|
+
robot_type: Optional[str] = None # ID of matched robot type
|
|
48
|
+
name: str = "" # Display name
|
|
49
|
+
status: DiscoveryStatus = DiscoveryStatus.FOUND
|
|
50
|
+
|
|
51
|
+
# Connection info
|
|
52
|
+
connection: Optional[ConnectionType] = None
|
|
53
|
+
port: Optional[str] = None # Serial port
|
|
54
|
+
ip: Optional[str] = None # Network IP
|
|
55
|
+
ports: Dict[str, int] = field(default_factory=dict) # Network ports
|
|
56
|
+
|
|
57
|
+
# Additional info
|
|
58
|
+
manufacturer: Optional[str] = None
|
|
59
|
+
model: Optional[str] = None
|
|
60
|
+
firmware: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
# Raw data
|
|
63
|
+
raw_data: Dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def discover_robots(
|
|
67
|
+
timeout: float = 5.0,
|
|
68
|
+
scan_serial: bool = True,
|
|
69
|
+
scan_network: bool = True,
|
|
70
|
+
network_subnet: Optional[str] = None,
|
|
71
|
+
) -> List[DiscoveredRobot]:
|
|
72
|
+
"""
|
|
73
|
+
Discover all robots on network and USB.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
timeout: Timeout for network scans
|
|
77
|
+
scan_serial: Scan USB serial devices
|
|
78
|
+
scan_network: Scan network for cameras/robots
|
|
79
|
+
network_subnet: Subnet to scan (e.g., "192.168.1")
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of discovered robots
|
|
83
|
+
"""
|
|
84
|
+
discovered = []
|
|
85
|
+
|
|
86
|
+
if scan_serial:
|
|
87
|
+
discovered.extend(discover_serial_robots())
|
|
88
|
+
|
|
89
|
+
if scan_network:
|
|
90
|
+
discovered.extend(discover_network_cameras(
|
|
91
|
+
timeout=timeout,
|
|
92
|
+
subnet=network_subnet
|
|
93
|
+
))
|
|
94
|
+
|
|
95
|
+
return discovered
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def discover_serial_robots() -> List[DiscoveredRobot]:
|
|
99
|
+
"""
|
|
100
|
+
Discover robots connected via USB serial.
|
|
101
|
+
|
|
102
|
+
Matches connected serial devices against known robot patterns.
|
|
103
|
+
"""
|
|
104
|
+
if not HAS_SERIAL:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
discovered = []
|
|
108
|
+
ports = serial.tools.list_ports.comports()
|
|
109
|
+
|
|
110
|
+
for port in ports:
|
|
111
|
+
robot = DiscoveredRobot(
|
|
112
|
+
connection=ConnectionType.SERIAL,
|
|
113
|
+
port=port.device,
|
|
114
|
+
raw_data={
|
|
115
|
+
"vid": port.vid,
|
|
116
|
+
"pid": port.pid,
|
|
117
|
+
"serial_number": port.serial_number,
|
|
118
|
+
"manufacturer": port.manufacturer,
|
|
119
|
+
"product": port.product,
|
|
120
|
+
"description": port.description,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Try to identify robot type by matching patterns
|
|
125
|
+
for robot_type in KNOWN_ROBOTS.values():
|
|
126
|
+
if ConnectionType.SERIAL not in robot_type.connection_types:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
for pattern in robot_type.serial_patterns:
|
|
130
|
+
# Convert glob pattern to check
|
|
131
|
+
if _matches_pattern(port.device, pattern):
|
|
132
|
+
robot.robot_type = robot_type.id
|
|
133
|
+
robot.name = f"{robot_type.name} ({port.device})"
|
|
134
|
+
robot.manufacturer = robot_type.manufacturer
|
|
135
|
+
robot.model = robot_type.name
|
|
136
|
+
robot.status = DiscoveryStatus.IDENTIFIED
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if robot.robot_type:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if not robot.robot_type:
|
|
143
|
+
robot.name = f"Unknown Device ({port.device})"
|
|
144
|
+
robot.manufacturer = port.manufacturer
|
|
145
|
+
robot.model = port.product
|
|
146
|
+
|
|
147
|
+
discovered.append(robot)
|
|
148
|
+
|
|
149
|
+
return discovered
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def discover_network_cameras(
|
|
153
|
+
timeout: float = 2.0,
|
|
154
|
+
subnet: Optional[str] = None,
|
|
155
|
+
max_workers: int = 50,
|
|
156
|
+
) -> List[DiscoveredRobot]:
|
|
157
|
+
"""
|
|
158
|
+
Discover network cameras (ESP32-CAM, IP cameras).
|
|
159
|
+
|
|
160
|
+
Scans common camera endpoints on the local subnet.
|
|
161
|
+
"""
|
|
162
|
+
if not HAS_REQUESTS:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
# Determine subnet to scan
|
|
166
|
+
if subnet is None:
|
|
167
|
+
subnet = _get_local_subnet()
|
|
168
|
+
if subnet is None:
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
discovered = []
|
|
172
|
+
ips_to_scan = [f"{subnet}.{i}" for i in range(1, 255)]
|
|
173
|
+
|
|
174
|
+
def check_camera(ip: str) -> Optional[DiscoveredRobot]:
|
|
175
|
+
"""Check if IP has a camera endpoint."""
|
|
176
|
+
try:
|
|
177
|
+
# Try ESP32-CAM status endpoint
|
|
178
|
+
response = requests.get(
|
|
179
|
+
f"http://{ip}/status",
|
|
180
|
+
timeout=timeout
|
|
181
|
+
)
|
|
182
|
+
if response.status_code == 200:
|
|
183
|
+
return DiscoveredRobot(
|
|
184
|
+
robot_type=None, # Camera only, not a robot
|
|
185
|
+
name=f"ESP32-CAM ({ip})",
|
|
186
|
+
status=DiscoveryStatus.FOUND,
|
|
187
|
+
connection=ConnectionType.WIFI,
|
|
188
|
+
ip=ip,
|
|
189
|
+
ports={"camera_port": 80, "camera_stream_port": 81},
|
|
190
|
+
raw_data={"type": "esp32cam", "response": response.text[:200]},
|
|
191
|
+
)
|
|
192
|
+
except requests.RequestException:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Try generic camera snapshot
|
|
197
|
+
response = requests.get(
|
|
198
|
+
f"http://{ip}/capture",
|
|
199
|
+
timeout=timeout,
|
|
200
|
+
stream=True
|
|
201
|
+
)
|
|
202
|
+
if response.status_code == 200:
|
|
203
|
+
content_type = response.headers.get("Content-Type", "")
|
|
204
|
+
if "image" in content_type:
|
|
205
|
+
return DiscoveredRobot(
|
|
206
|
+
name=f"Network Camera ({ip})",
|
|
207
|
+
status=DiscoveryStatus.FOUND,
|
|
208
|
+
connection=ConnectionType.WIFI,
|
|
209
|
+
ip=ip,
|
|
210
|
+
ports={"camera_port": 80},
|
|
211
|
+
raw_data={"type": "generic_camera"},
|
|
212
|
+
)
|
|
213
|
+
except requests.RequestException:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
# Parallel scan
|
|
219
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
220
|
+
futures = {executor.submit(check_camera, ip): ip for ip in ips_to_scan}
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
for future in concurrent.futures.as_completed(futures, timeout=timeout * 3):
|
|
224
|
+
try:
|
|
225
|
+
result = future.result(timeout=0.1)
|
|
226
|
+
if result:
|
|
227
|
+
discovered.append(result)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
except TimeoutError:
|
|
231
|
+
# Some futures didn't complete in time, that's ok
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
return discovered
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def probe_serial_device(port: str, baud_rate: int = 115200) -> Optional[Dict[str, Any]]:
|
|
238
|
+
"""
|
|
239
|
+
Probe a serial device to identify what type of robot it is.
|
|
240
|
+
|
|
241
|
+
Sends identification commands and parses response.
|
|
242
|
+
"""
|
|
243
|
+
if not HAS_SERIAL:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
with serial.Serial(port, baud_rate, timeout=2) as ser:
|
|
248
|
+
time.sleep(0.5)
|
|
249
|
+
|
|
250
|
+
# Try MicroPython REPL identification
|
|
251
|
+
ser.write(b'\x03') # Ctrl+C
|
|
252
|
+
time.sleep(0.2)
|
|
253
|
+
ser.write(b'\x02') # Ctrl+B for friendly REPL
|
|
254
|
+
time.sleep(0.3)
|
|
255
|
+
|
|
256
|
+
# Check for MicroPython
|
|
257
|
+
ser.write(b'import sys; print(sys.implementation)\r\n')
|
|
258
|
+
time.sleep(0.5)
|
|
259
|
+
response = ser.read(1000).decode('utf-8', errors='ignore')
|
|
260
|
+
|
|
261
|
+
if 'micropython' in response.lower():
|
|
262
|
+
info = {"type": "micropython", "response": response}
|
|
263
|
+
|
|
264
|
+
# Try to detect MechDog
|
|
265
|
+
ser.write(b'from HW_MechDog import MechDog\r\n')
|
|
266
|
+
time.sleep(0.3)
|
|
267
|
+
response2 = ser.read(500).decode('utf-8', errors='ignore')
|
|
268
|
+
|
|
269
|
+
if 'Error' not in response2 and 'Traceback' not in response2:
|
|
270
|
+
info["robot"] = "hiwonder_mechdog"
|
|
271
|
+
|
|
272
|
+
return info
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return {"error": str(e)}
|
|
276
|
+
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _matches_pattern(device: str, pattern: str) -> bool:
|
|
281
|
+
"""Check if device matches a glob pattern."""
|
|
282
|
+
import fnmatch
|
|
283
|
+
return fnmatch.fnmatch(device, pattern)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _get_local_subnet() -> Optional[str]:
|
|
287
|
+
"""Get the local network subnet (e.g., '192.168.1')."""
|
|
288
|
+
system = platform.system()
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
import socket
|
|
292
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
293
|
+
s.connect(("8.8.8.8", 80))
|
|
294
|
+
ip = s.getsockname()[0]
|
|
295
|
+
s.close()
|
|
296
|
+
|
|
297
|
+
# Extract subnet
|
|
298
|
+
parts = ip.split(".")
|
|
299
|
+
if len(parts) == 4:
|
|
300
|
+
return ".".join(parts[:3])
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def quick_scan() -> Dict[str, Any]:
|
|
308
|
+
"""
|
|
309
|
+
Quick scan for common robot configurations.
|
|
310
|
+
|
|
311
|
+
Returns summary of what was found.
|
|
312
|
+
"""
|
|
313
|
+
result = {
|
|
314
|
+
"serial_ports": [],
|
|
315
|
+
"network_cameras": [],
|
|
316
|
+
"identified_robots": [],
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Scan serial
|
|
320
|
+
serial_robots = discover_serial_robots()
|
|
321
|
+
for robot in serial_robots:
|
|
322
|
+
result["serial_ports"].append({
|
|
323
|
+
"port": robot.port,
|
|
324
|
+
"type": robot.robot_type,
|
|
325
|
+
"name": robot.name,
|
|
326
|
+
})
|
|
327
|
+
if robot.robot_type:
|
|
328
|
+
result["identified_robots"].append(robot)
|
|
329
|
+
|
|
330
|
+
# Quick network scan (just local subnet, fast timeout)
|
|
331
|
+
network_cameras = discover_network_cameras(timeout=1.0)
|
|
332
|
+
for camera in network_cameras:
|
|
333
|
+
result["network_cameras"].append({
|
|
334
|
+
"ip": camera.ip,
|
|
335
|
+
"name": camera.name,
|
|
336
|
+
"ports": camera.ports,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
return result
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Robot capability introspection.
|
|
3
|
+
|
|
4
|
+
Inspect what a robot can do by examining its interfaces and methods.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
from typing import Dict, List, Set, Any, Optional, Type
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from ..interfaces import (
|
|
12
|
+
RobotInterface,
|
|
13
|
+
SafetyInterface,
|
|
14
|
+
QuadrupedLocomotion,
|
|
15
|
+
BipedLocomotion,
|
|
16
|
+
WheeledLocomotion,
|
|
17
|
+
AerialLocomotion,
|
|
18
|
+
ArmInterface,
|
|
19
|
+
GripperInterface,
|
|
20
|
+
CameraInterface,
|
|
21
|
+
DepthCameraInterface,
|
|
22
|
+
LidarInterface,
|
|
23
|
+
IMUInterface,
|
|
24
|
+
BodyPoseInterface,
|
|
25
|
+
NavigationInterface,
|
|
26
|
+
ObjectDetectionInterface,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MethodInfo:
|
|
32
|
+
"""Information about a robot method."""
|
|
33
|
+
name: str
|
|
34
|
+
interface: str
|
|
35
|
+
description: str
|
|
36
|
+
parameters: List[str]
|
|
37
|
+
return_type: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CapabilityInfo:
|
|
42
|
+
"""Information about a robot capability."""
|
|
43
|
+
name: str
|
|
44
|
+
interface_class: str
|
|
45
|
+
description: str
|
|
46
|
+
methods: List[MethodInfo]
|
|
47
|
+
available: bool = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Map interfaces to capability names
|
|
51
|
+
INTERFACE_CAPABILITIES = {
|
|
52
|
+
QuadrupedLocomotion: "quadruped_locomotion",
|
|
53
|
+
BipedLocomotion: "bipedal_locomotion",
|
|
54
|
+
WheeledLocomotion: "wheeled_locomotion",
|
|
55
|
+
AerialLocomotion: "aerial_locomotion",
|
|
56
|
+
ArmInterface: "arm",
|
|
57
|
+
GripperInterface: "gripper",
|
|
58
|
+
CameraInterface: "camera",
|
|
59
|
+
DepthCameraInterface: "depth_camera",
|
|
60
|
+
LidarInterface: "lidar",
|
|
61
|
+
IMUInterface: "imu",
|
|
62
|
+
BodyPoseInterface: "body_pose",
|
|
63
|
+
SafetyInterface: "safety",
|
|
64
|
+
NavigationInterface: "navigation",
|
|
65
|
+
ObjectDetectionInterface: "object_detection",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_capabilities(robot: Any) -> Dict[str, CapabilityInfo]:
|
|
70
|
+
"""
|
|
71
|
+
Get all capabilities of a robot instance.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
robot: Robot instance
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict mapping capability name to CapabilityInfo
|
|
78
|
+
"""
|
|
79
|
+
capabilities = {}
|
|
80
|
+
|
|
81
|
+
for interface, cap_name in INTERFACE_CAPABILITIES.items():
|
|
82
|
+
if isinstance(robot, interface):
|
|
83
|
+
methods = get_interface_methods(interface)
|
|
84
|
+
capabilities[cap_name] = CapabilityInfo(
|
|
85
|
+
name=cap_name,
|
|
86
|
+
interface_class=interface.__name__,
|
|
87
|
+
description=interface.__doc__ or "",
|
|
88
|
+
methods=methods,
|
|
89
|
+
available=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return capabilities
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_methods(robot: Any) -> Dict[str, List[MethodInfo]]:
|
|
96
|
+
"""
|
|
97
|
+
Get all available methods grouped by capability.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
robot: Robot instance
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict mapping capability name to list of methods
|
|
104
|
+
"""
|
|
105
|
+
result = {}
|
|
106
|
+
|
|
107
|
+
for interface, cap_name in INTERFACE_CAPABILITIES.items():
|
|
108
|
+
if isinstance(robot, interface):
|
|
109
|
+
methods = get_interface_methods(interface)
|
|
110
|
+
if methods:
|
|
111
|
+
result[cap_name] = methods
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_interface_methods(interface: Type) -> List[MethodInfo]:
|
|
117
|
+
"""
|
|
118
|
+
Get methods defined by an interface.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
interface: Interface class
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of MethodInfo
|
|
125
|
+
"""
|
|
126
|
+
methods = []
|
|
127
|
+
|
|
128
|
+
for name, method in inspect.getmembers(interface, predicate=inspect.isfunction):
|
|
129
|
+
if name.startswith("_"):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Get signature
|
|
133
|
+
try:
|
|
134
|
+
sig = inspect.signature(method)
|
|
135
|
+
params = [
|
|
136
|
+
p.name for p in sig.parameters.values()
|
|
137
|
+
if p.name != "self"
|
|
138
|
+
]
|
|
139
|
+
return_type = str(sig.return_annotation) if sig.return_annotation != inspect.Signature.empty else "None"
|
|
140
|
+
except (ValueError, TypeError):
|
|
141
|
+
params = []
|
|
142
|
+
return_type = "Unknown"
|
|
143
|
+
|
|
144
|
+
# Clean up return type
|
|
145
|
+
return_type = return_type.replace("typing.", "").replace("<class '", "").replace("'>", "")
|
|
146
|
+
|
|
147
|
+
methods.append(MethodInfo(
|
|
148
|
+
name=name,
|
|
149
|
+
interface=interface.__name__,
|
|
150
|
+
description=method.__doc__ or "",
|
|
151
|
+
parameters=params,
|
|
152
|
+
return_type=return_type,
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
return methods
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_capability(robot: Any, capability: str) -> Dict[str, Any]:
|
|
159
|
+
"""
|
|
160
|
+
Test if a capability is working.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
robot: Robot instance
|
|
164
|
+
capability: Capability name to test
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Test result with status and details
|
|
168
|
+
"""
|
|
169
|
+
result = {
|
|
170
|
+
"capability": capability,
|
|
171
|
+
"status": "unknown",
|
|
172
|
+
"tests": [],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Find the interface for this capability
|
|
176
|
+
interface = None
|
|
177
|
+
for iface, cap_name in INTERFACE_CAPABILITIES.items():
|
|
178
|
+
if cap_name == capability:
|
|
179
|
+
interface = iface
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
if interface is None:
|
|
183
|
+
result["status"] = "error"
|
|
184
|
+
result["error"] = f"Unknown capability: {capability}"
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
if not isinstance(robot, interface):
|
|
188
|
+
result["status"] = "not_available"
|
|
189
|
+
result["error"] = f"Robot does not have capability: {capability}"
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
# Run capability-specific tests
|
|
193
|
+
try:
|
|
194
|
+
if capability == "camera":
|
|
195
|
+
result["tests"].append(_test_camera(robot))
|
|
196
|
+
elif capability == "quadruped_locomotion":
|
|
197
|
+
result["tests"].append(_test_locomotion(robot))
|
|
198
|
+
elif capability == "body_pose":
|
|
199
|
+
result["tests"].append(_test_body_pose(robot))
|
|
200
|
+
elif capability == "safety":
|
|
201
|
+
result["tests"].append(_test_safety(robot))
|
|
202
|
+
else:
|
|
203
|
+
result["tests"].append({
|
|
204
|
+
"name": "basic_check",
|
|
205
|
+
"status": "passed",
|
|
206
|
+
"message": f"Robot implements {interface.__name__}",
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
# Determine overall status
|
|
210
|
+
if all(t.get("status") == "passed" for t in result["tests"]):
|
|
211
|
+
result["status"] = "passed"
|
|
212
|
+
elif any(t.get("status") == "failed" for t in result["tests"]):
|
|
213
|
+
result["status"] = "failed"
|
|
214
|
+
else:
|
|
215
|
+
result["status"] = "partial"
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
result["status"] = "error"
|
|
219
|
+
result["error"] = str(e)
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _test_camera(robot: Any) -> Dict[str, Any]:
|
|
225
|
+
"""Test camera capability."""
|
|
226
|
+
test = {"name": "camera_capture", "status": "unknown"}
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
image = robot.get_image()
|
|
230
|
+
if image.width > 0 and image.height > 0:
|
|
231
|
+
test["status"] = "passed"
|
|
232
|
+
test["message"] = f"Captured {image.width}x{image.height} image"
|
|
233
|
+
else:
|
|
234
|
+
test["status"] = "failed"
|
|
235
|
+
test["message"] = "Image capture returned empty"
|
|
236
|
+
except Exception as e:
|
|
237
|
+
test["status"] = "failed"
|
|
238
|
+
test["error"] = str(e)
|
|
239
|
+
|
|
240
|
+
return test
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _test_locomotion(robot: Any) -> Dict[str, Any]:
|
|
244
|
+
"""Test locomotion capability (non-destructive)."""
|
|
245
|
+
test = {"name": "locomotion_status", "status": "unknown"}
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
if hasattr(robot, "is_moving"):
|
|
249
|
+
is_moving = robot.is_moving()
|
|
250
|
+
test["status"] = "passed"
|
|
251
|
+
test["message"] = f"Robot is {'moving' if is_moving else 'stationary'}"
|
|
252
|
+
else:
|
|
253
|
+
test["status"] = "passed"
|
|
254
|
+
test["message"] = "Locomotion interface available"
|
|
255
|
+
except Exception as e:
|
|
256
|
+
test["status"] = "failed"
|
|
257
|
+
test["error"] = str(e)
|
|
258
|
+
|
|
259
|
+
return test
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _test_body_pose(robot: Any) -> Dict[str, Any]:
|
|
263
|
+
"""Test body pose capability."""
|
|
264
|
+
test = {"name": "body_pose_read", "status": "unknown"}
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
height = robot.get_body_height()
|
|
268
|
+
test["status"] = "passed"
|
|
269
|
+
test["message"] = f"Body height: {height:.3f}m"
|
|
270
|
+
except Exception as e:
|
|
271
|
+
test["status"] = "failed"
|
|
272
|
+
test["error"] = str(e)
|
|
273
|
+
|
|
274
|
+
return test
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _test_safety(robot: Any) -> Dict[str, Any]:
|
|
278
|
+
"""Test safety capability."""
|
|
279
|
+
test = {"name": "safety_status", "status": "unknown"}
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
estopped = robot.is_estopped()
|
|
283
|
+
test["status"] = "passed"
|
|
284
|
+
test["message"] = f"E-stop: {'active' if estopped else 'inactive'}"
|
|
285
|
+
except Exception as e:
|
|
286
|
+
test["status"] = "failed"
|
|
287
|
+
test["error"] = str(e)
|
|
288
|
+
|
|
289
|
+
return test
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def format_capabilities(capabilities: Dict[str, CapabilityInfo]) -> str:
|
|
293
|
+
"""Format capabilities for CLI display."""
|
|
294
|
+
lines = []
|
|
295
|
+
|
|
296
|
+
for name, info in capabilities.items():
|
|
297
|
+
lines.append(f"\n{name.upper()}")
|
|
298
|
+
lines.append("-" * len(name))
|
|
299
|
+
|
|
300
|
+
if info.description:
|
|
301
|
+
first_line = info.description.strip().split("\n")[0]
|
|
302
|
+
lines.append(f" {first_line}")
|
|
303
|
+
|
|
304
|
+
lines.append(" Methods:")
|
|
305
|
+
for method in info.methods[:5]: # Show first 5 methods
|
|
306
|
+
params = ", ".join(method.parameters[:3]) # Show first 3 params
|
|
307
|
+
if len(method.parameters) > 3:
|
|
308
|
+
params += ", ..."
|
|
309
|
+
lines.append(f" - {method.name}({params})")
|
|
310
|
+
|
|
311
|
+
if len(info.methods) > 5:
|
|
312
|
+
lines.append(f" ... and {len(info.methods) - 5} more")
|
|
313
|
+
|
|
314
|
+
return "\n".join(lines)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def format_methods(methods: Dict[str, List[MethodInfo]]) -> str:
|
|
318
|
+
"""Format methods for CLI display."""
|
|
319
|
+
lines = []
|
|
320
|
+
|
|
321
|
+
for capability, method_list in methods.items():
|
|
322
|
+
lines.append(f"\n[{capability}]")
|
|
323
|
+
for method in method_list:
|
|
324
|
+
params = ", ".join(method.parameters)
|
|
325
|
+
lines.append(f" {method.name}({params}) -> {method.return_type}")
|
|
326
|
+
if method.description:
|
|
327
|
+
first_line = method.description.strip().split("\n")[0][:60]
|
|
328
|
+
lines.append(f" {first_line}")
|
|
329
|
+
|
|
330
|
+
return "\n".join(lines)
|