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.
Files changed (131) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +100 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/behaviors/common.py +686 -0
  9. ate/behaviors/tree.py +454 -0
  10. ate/cli.py +855 -3995
  11. ate/client.py +90 -0
  12. ate/commands/__init__.py +168 -0
  13. ate/commands/auth.py +389 -0
  14. ate/commands/bridge.py +448 -0
  15. ate/commands/data.py +185 -0
  16. ate/commands/deps.py +111 -0
  17. ate/commands/generate.py +384 -0
  18. ate/commands/memory.py +907 -0
  19. ate/commands/parts.py +166 -0
  20. ate/commands/primitive.py +399 -0
  21. ate/commands/protocol.py +288 -0
  22. ate/commands/recording.py +524 -0
  23. ate/commands/repo.py +154 -0
  24. ate/commands/simulation.py +291 -0
  25. ate/commands/skill.py +303 -0
  26. ate/commands/skills.py +487 -0
  27. ate/commands/team.py +147 -0
  28. ate/commands/workflow.py +271 -0
  29. ate/detection/__init__.py +38 -0
  30. ate/detection/base.py +142 -0
  31. ate/detection/color_detector.py +399 -0
  32. ate/detection/trash_detector.py +322 -0
  33. ate/drivers/__init__.py +39 -0
  34. ate/drivers/ble_transport.py +405 -0
  35. ate/drivers/mechdog.py +942 -0
  36. ate/drivers/wifi_camera.py +477 -0
  37. ate/interfaces/__init__.py +187 -0
  38. ate/interfaces/base.py +273 -0
  39. ate/interfaces/body.py +267 -0
  40. ate/interfaces/detection.py +282 -0
  41. ate/interfaces/locomotion.py +422 -0
  42. ate/interfaces/manipulation.py +408 -0
  43. ate/interfaces/navigation.py +389 -0
  44. ate/interfaces/perception.py +362 -0
  45. ate/interfaces/sensors.py +247 -0
  46. ate/interfaces/types.py +371 -0
  47. ate/llm_proxy.py +239 -0
  48. ate/mcp_server.py +387 -0
  49. ate/memory/__init__.py +35 -0
  50. ate/memory/cloud.py +244 -0
  51. ate/memory/context.py +269 -0
  52. ate/memory/embeddings.py +184 -0
  53. ate/memory/export.py +26 -0
  54. ate/memory/merge.py +146 -0
  55. ate/memory/migrate/__init__.py +34 -0
  56. ate/memory/migrate/base.py +89 -0
  57. ate/memory/migrate/pipeline.py +189 -0
  58. ate/memory/migrate/sources/__init__.py +13 -0
  59. ate/memory/migrate/sources/chroma.py +170 -0
  60. ate/memory/migrate/sources/pinecone.py +120 -0
  61. ate/memory/migrate/sources/qdrant.py +110 -0
  62. ate/memory/migrate/sources/weaviate.py +160 -0
  63. ate/memory/reranker.py +353 -0
  64. ate/memory/search.py +26 -0
  65. ate/memory/store.py +548 -0
  66. ate/recording/__init__.py +83 -0
  67. ate/recording/demonstration.py +378 -0
  68. ate/recording/session.py +415 -0
  69. ate/recording/upload.py +304 -0
  70. ate/recording/visual.py +416 -0
  71. ate/recording/wrapper.py +95 -0
  72. ate/robot/__init__.py +221 -0
  73. ate/robot/agentic_servo.py +856 -0
  74. ate/robot/behaviors.py +493 -0
  75. ate/robot/ble_capture.py +1000 -0
  76. ate/robot/ble_enumerate.py +506 -0
  77. ate/robot/calibration.py +668 -0
  78. ate/robot/calibration_state.py +388 -0
  79. ate/robot/commands.py +3735 -0
  80. ate/robot/direction_calibration.py +554 -0
  81. ate/robot/discovery.py +441 -0
  82. ate/robot/introspection.py +330 -0
  83. ate/robot/llm_system_id.py +654 -0
  84. ate/robot/locomotion_calibration.py +508 -0
  85. ate/robot/manager.py +270 -0
  86. ate/robot/marker_generator.py +611 -0
  87. ate/robot/perception.py +502 -0
  88. ate/robot/primitives.py +614 -0
  89. ate/robot/profiles.py +281 -0
  90. ate/robot/registry.py +322 -0
  91. ate/robot/servo_mapper.py +1153 -0
  92. ate/robot/skill_upload.py +675 -0
  93. ate/robot/target_calibration.py +500 -0
  94. ate/robot/teach.py +515 -0
  95. ate/robot/types.py +242 -0
  96. ate/robot/visual_labeler.py +1048 -0
  97. ate/robot/visual_servo_loop.py +494 -0
  98. ate/robot/visual_servoing.py +570 -0
  99. ate/robot/visual_system_id.py +906 -0
  100. ate/transports/__init__.py +121 -0
  101. ate/transports/base.py +394 -0
  102. ate/transports/ble.py +405 -0
  103. ate/transports/hybrid.py +444 -0
  104. ate/transports/serial.py +345 -0
  105. ate/urdf/__init__.py +30 -0
  106. ate/urdf/capture.py +582 -0
  107. ate/urdf/cloud.py +491 -0
  108. ate/urdf/collision.py +271 -0
  109. ate/urdf/commands.py +708 -0
  110. ate/urdf/depth.py +360 -0
  111. ate/urdf/inertial.py +312 -0
  112. ate/urdf/kinematics.py +330 -0
  113. ate/urdf/lifting.py +415 -0
  114. ate/urdf/meshing.py +300 -0
  115. ate/urdf/models/__init__.py +110 -0
  116. ate/urdf/models/depth_anything.py +253 -0
  117. ate/urdf/models/sam2.py +324 -0
  118. ate/urdf/motion_analysis.py +396 -0
  119. ate/urdf/pipeline.py +468 -0
  120. ate/urdf/scale.py +256 -0
  121. ate/urdf/scan_session.py +411 -0
  122. ate/urdf/segmentation.py +299 -0
  123. ate/urdf/synthesis.py +319 -0
  124. ate/urdf/topology.py +336 -0
  125. ate/urdf/validation.py +371 -0
  126. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
  127. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  128. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  129. foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
  130. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  131. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/robot/discovery.py ADDED
@@ -0,0 +1,441 @@
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
+ - BLE devices (ESP32, HM-10, etc.)
8
+ - ROS2 topics (if ROS2 available)
9
+ - mDNS services
10
+ """
11
+
12
+ import os
13
+ import glob
14
+ import time
15
+ import platform
16
+ import concurrent.futures
17
+ from dataclasses import dataclass, field
18
+ from typing import List, Optional, Dict, Any
19
+ from enum import Enum, auto
20
+
21
+ try:
22
+ import serial
23
+ import serial.tools.list_ports
24
+ HAS_SERIAL = True
25
+ except ImportError:
26
+ HAS_SERIAL = False
27
+
28
+ try:
29
+ import requests
30
+ HAS_REQUESTS = True
31
+ except ImportError:
32
+ HAS_REQUESTS = False
33
+
34
+ try:
35
+ from bleak import BleakScanner
36
+ HAS_BLEAK = True
37
+ except ImportError:
38
+ HAS_BLEAK = False
39
+
40
+ from .registry import KNOWN_ROBOTS, RobotType, ConnectionType
41
+
42
+
43
+ class DiscoveryStatus(Enum):
44
+ """Status of discovered device."""
45
+ FOUND = auto() # Device found
46
+ IDENTIFIED = auto() # Device identified as known robot
47
+ CONNECTED = auto() # Successfully connected
48
+ ERROR = auto() # Error during discovery
49
+
50
+
51
+ @dataclass
52
+ class DiscoveredRobot:
53
+ """A robot discovered on the network or USB."""
54
+ robot_type: Optional[str] = None # ID of matched robot type
55
+ name: str = "" # Display name
56
+ status: DiscoveryStatus = DiscoveryStatus.FOUND
57
+
58
+ # Connection info
59
+ connection: Optional[ConnectionType] = None
60
+ port: Optional[str] = None # Serial port
61
+ ip: Optional[str] = None # Network IP
62
+ ports: Dict[str, int] = field(default_factory=dict) # Network ports
63
+
64
+ # Additional info
65
+ manufacturer: Optional[str] = None
66
+ model: Optional[str] = None
67
+ firmware: Optional[str] = None
68
+
69
+ # Raw data
70
+ raw_data: Dict[str, Any] = field(default_factory=dict)
71
+
72
+
73
+ def discover_robots(
74
+ timeout: float = 5.0,
75
+ scan_serial: bool = True,
76
+ scan_network: bool = True,
77
+ scan_ble: bool = True,
78
+ network_subnet: Optional[str] = None,
79
+ ) -> List[DiscoveredRobot]:
80
+ """
81
+ Discover all robots on network, USB, and BLE.
82
+
83
+ Args:
84
+ timeout: Timeout for network/BLE scans
85
+ scan_serial: Scan USB serial devices
86
+ scan_network: Scan network for cameras/robots
87
+ scan_ble: Scan for BLE devices
88
+ network_subnet: Subnet to scan (e.g., "192.168.1")
89
+
90
+ Returns:
91
+ List of discovered robots
92
+ """
93
+ discovered = []
94
+
95
+ if scan_serial:
96
+ discovered.extend(discover_serial_robots())
97
+
98
+ if scan_network:
99
+ discovered.extend(discover_network_cameras(
100
+ timeout=timeout,
101
+ subnet=network_subnet
102
+ ))
103
+
104
+ if scan_ble:
105
+ discovered.extend(discover_ble_robots(timeout=timeout))
106
+
107
+ return discovered
108
+
109
+
110
+ def discover_serial_robots() -> List[DiscoveredRobot]:
111
+ """
112
+ Discover robots connected via USB serial.
113
+
114
+ Matches connected serial devices against known robot patterns.
115
+ """
116
+ if not HAS_SERIAL:
117
+ return []
118
+
119
+ discovered = []
120
+ ports = serial.tools.list_ports.comports()
121
+
122
+ for port in ports:
123
+ robot = DiscoveredRobot(
124
+ connection=ConnectionType.SERIAL,
125
+ port=port.device,
126
+ raw_data={
127
+ "vid": port.vid,
128
+ "pid": port.pid,
129
+ "serial_number": port.serial_number,
130
+ "manufacturer": port.manufacturer,
131
+ "product": port.product,
132
+ "description": port.description,
133
+ }
134
+ )
135
+
136
+ # Try to identify robot type by matching patterns
137
+ for robot_type in KNOWN_ROBOTS.values():
138
+ if ConnectionType.SERIAL not in robot_type.connection_types:
139
+ continue
140
+
141
+ for pattern in robot_type.serial_patterns:
142
+ # Convert glob pattern to check
143
+ if _matches_pattern(port.device, pattern):
144
+ robot.robot_type = robot_type.id
145
+ robot.name = f"{robot_type.name} ({port.device})"
146
+ robot.manufacturer = robot_type.manufacturer
147
+ robot.model = robot_type.name
148
+ robot.status = DiscoveryStatus.IDENTIFIED
149
+ break
150
+
151
+ if robot.robot_type:
152
+ break
153
+
154
+ if not robot.robot_type:
155
+ robot.name = f"Unknown Device ({port.device})"
156
+ robot.manufacturer = port.manufacturer
157
+ robot.model = port.product
158
+
159
+ discovered.append(robot)
160
+
161
+ return discovered
162
+
163
+
164
+ def discover_network_cameras(
165
+ timeout: float = 2.0,
166
+ subnet: Optional[str] = None,
167
+ max_workers: int = 50,
168
+ ) -> List[DiscoveredRobot]:
169
+ """
170
+ Discover network cameras (ESP32-CAM, IP cameras).
171
+
172
+ Scans common camera endpoints on the local subnet.
173
+ """
174
+ if not HAS_REQUESTS:
175
+ return []
176
+
177
+ # Determine subnet to scan
178
+ if subnet is None:
179
+ subnet = _get_local_subnet()
180
+ if subnet is None:
181
+ return []
182
+
183
+ discovered = []
184
+ ips_to_scan = [f"{subnet}.{i}" for i in range(1, 255)]
185
+
186
+ def check_camera(ip: str) -> Optional[DiscoveredRobot]:
187
+ """Check if IP has a camera endpoint."""
188
+ try:
189
+ # Try ESP32-CAM status endpoint
190
+ response = requests.get(
191
+ f"http://{ip}/status",
192
+ timeout=timeout
193
+ )
194
+ if response.status_code == 200:
195
+ return DiscoveredRobot(
196
+ robot_type=None, # Camera only, not a robot
197
+ name=f"ESP32-CAM ({ip})",
198
+ status=DiscoveryStatus.FOUND,
199
+ connection=ConnectionType.WIFI,
200
+ ip=ip,
201
+ ports={"camera_port": 80, "camera_stream_port": 81},
202
+ raw_data={"type": "esp32cam", "response": response.text[:200]},
203
+ )
204
+ except requests.RequestException:
205
+ pass
206
+
207
+ try:
208
+ # Try generic camera snapshot
209
+ response = requests.get(
210
+ f"http://{ip}/capture",
211
+ timeout=timeout,
212
+ stream=True
213
+ )
214
+ if response.status_code == 200:
215
+ content_type = response.headers.get("Content-Type", "")
216
+ if "image" in content_type:
217
+ return DiscoveredRobot(
218
+ name=f"Network Camera ({ip})",
219
+ status=DiscoveryStatus.FOUND,
220
+ connection=ConnectionType.WIFI,
221
+ ip=ip,
222
+ ports={"camera_port": 80},
223
+ raw_data={"type": "generic_camera"},
224
+ )
225
+ except requests.RequestException:
226
+ pass
227
+
228
+ return None
229
+
230
+ # Parallel scan
231
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
232
+ futures = {executor.submit(check_camera, ip): ip for ip in ips_to_scan}
233
+
234
+ try:
235
+ for future in concurrent.futures.as_completed(futures, timeout=timeout * 3):
236
+ try:
237
+ result = future.result(timeout=0.1)
238
+ if result:
239
+ discovered.append(result)
240
+ except Exception:
241
+ pass
242
+ except TimeoutError:
243
+ # Some futures didn't complete in time, that's ok
244
+ pass
245
+
246
+ return discovered
247
+
248
+
249
+ def discover_ble_robots(
250
+ timeout: float = 10.0,
251
+ name_patterns: List[str] = None,
252
+ ) -> List[DiscoveredRobot]:
253
+ """
254
+ Discover robots via Bluetooth Low Energy.
255
+
256
+ Scans for BLE devices matching known robot name patterns.
257
+ Common ESP32-based robots advertise names like "MechDog", "ESP32", etc.
258
+ """
259
+ if not HAS_BLEAK:
260
+ return []
261
+
262
+ import asyncio
263
+
264
+ # Default patterns for known robots
265
+ if name_patterns is None:
266
+ name_patterns = [
267
+ "MechDog",
268
+ "HiWonder",
269
+ "ESP32",
270
+ "ESP-",
271
+ "Unitree",
272
+ "Robot",
273
+ ]
274
+
275
+ async def scan():
276
+ devices = await BleakScanner.discover(timeout=timeout)
277
+ return devices
278
+
279
+ try:
280
+ # Run the async scan
281
+ devices = asyncio.run(scan())
282
+ except Exception as e:
283
+ print(f"BLE scan error: {e}")
284
+ return []
285
+
286
+ discovered = []
287
+ for device in devices:
288
+ name = device.name or ""
289
+
290
+ # Check if name matches any pattern
291
+ matched_pattern = None
292
+ for pattern in name_patterns:
293
+ if pattern.lower() in name.lower():
294
+ matched_pattern = pattern
295
+ break
296
+
297
+ if not matched_pattern:
298
+ continue
299
+
300
+ # Try to identify robot type
301
+ robot_type = None
302
+ for rt in KNOWN_ROBOTS.values():
303
+ if ConnectionType.BLUETOOTH not in rt.connection_types:
304
+ continue
305
+ if rt.name.lower() in name.lower() or rt.manufacturer.lower() in name.lower():
306
+ robot_type = rt.id
307
+ break
308
+
309
+ robot = DiscoveredRobot(
310
+ robot_type=robot_type,
311
+ name=f"{name} ({device.address})",
312
+ status=DiscoveryStatus.IDENTIFIED if robot_type else DiscoveryStatus.FOUND,
313
+ connection=ConnectionType.BLUETOOTH,
314
+ port=device.address, # BLE address used as "port"
315
+ raw_data={
316
+ "ble_address": device.address,
317
+ "ble_name": name,
318
+ "rssi": device.rssi if hasattr(device, 'rssi') else None,
319
+ }
320
+ )
321
+ discovered.append(robot)
322
+
323
+ return discovered
324
+
325
+
326
+ def probe_serial_device(port: str, baud_rate: int = 115200) -> Optional[Dict[str, Any]]:
327
+ """
328
+ Probe a serial device to identify what type of robot it is.
329
+
330
+ Sends identification commands and parses response.
331
+ """
332
+ if not HAS_SERIAL:
333
+ return None
334
+
335
+ try:
336
+ with serial.Serial(port, baud_rate, timeout=2) as ser:
337
+ time.sleep(0.5)
338
+
339
+ # Try MicroPython REPL identification
340
+ ser.write(b'\x03') # Ctrl+C
341
+ time.sleep(0.2)
342
+ ser.write(b'\x02') # Ctrl+B for friendly REPL
343
+ time.sleep(0.3)
344
+
345
+ # Check for MicroPython
346
+ ser.write(b'import sys; print(sys.implementation)\r\n')
347
+ time.sleep(0.5)
348
+ response = ser.read(1000).decode('utf-8', errors='ignore')
349
+
350
+ if 'micropython' in response.lower():
351
+ info = {"type": "micropython", "response": response}
352
+
353
+ # Try to detect MechDog
354
+ ser.write(b'from HW_MechDog import MechDog\r\n')
355
+ time.sleep(0.3)
356
+ response2 = ser.read(500).decode('utf-8', errors='ignore')
357
+
358
+ if 'Error' not in response2 and 'Traceback' not in response2:
359
+ info["robot"] = "hiwonder_mechdog"
360
+
361
+ return info
362
+
363
+ except Exception as e:
364
+ return {"error": str(e)}
365
+
366
+ return None
367
+
368
+
369
+ def _matches_pattern(device: str, pattern: str) -> bool:
370
+ """Check if device matches a glob pattern."""
371
+ import fnmatch
372
+ return fnmatch.fnmatch(device, pattern)
373
+
374
+
375
+ def _get_local_subnet() -> Optional[str]:
376
+ """Get the local network subnet (e.g., '192.168.1')."""
377
+ system = platform.system()
378
+
379
+ try:
380
+ import socket
381
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
382
+ s.connect(("8.8.8.8", 80))
383
+ ip = s.getsockname()[0]
384
+ s.close()
385
+
386
+ # Extract subnet
387
+ parts = ip.split(".")
388
+ if len(parts) == 4:
389
+ return ".".join(parts[:3])
390
+ except Exception:
391
+ pass
392
+
393
+ return None
394
+
395
+
396
+ def quick_scan() -> Dict[str, Any]:
397
+ """
398
+ Quick scan for common robot configurations.
399
+
400
+ Returns summary of what was found.
401
+ """
402
+ result = {
403
+ "serial_ports": [],
404
+ "network_cameras": [],
405
+ "ble_devices": [],
406
+ "identified_robots": [],
407
+ }
408
+
409
+ # Scan serial
410
+ serial_robots = discover_serial_robots()
411
+ for robot in serial_robots:
412
+ result["serial_ports"].append({
413
+ "port": robot.port,
414
+ "type": robot.robot_type,
415
+ "name": robot.name,
416
+ })
417
+ if robot.robot_type:
418
+ result["identified_robots"].append(robot)
419
+
420
+ # Quick network scan (just local subnet, fast timeout)
421
+ network_cameras = discover_network_cameras(timeout=1.0)
422
+ for camera in network_cameras:
423
+ result["network_cameras"].append({
424
+ "ip": camera.ip,
425
+ "name": camera.name,
426
+ "ports": camera.ports,
427
+ })
428
+
429
+ # Quick BLE scan
430
+ ble_robots = discover_ble_robots(timeout=3.0)
431
+ for robot in ble_robots:
432
+ result["ble_devices"].append({
433
+ "address": robot.port,
434
+ "name": robot.name,
435
+ "type": robot.robot_type,
436
+ "rssi": robot.raw_data.get("rssi"),
437
+ })
438
+ if robot.robot_type:
439
+ result["identified_robots"].append(robot)
440
+
441
+ return result