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/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)