foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.1__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 (116) 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 +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +402 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.1.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,405 @@
1
+ """
2
+ BLE Transport Layer for Robot Drivers.
3
+
4
+ Provides a serial-compatible interface over Bluetooth Low Energy.
5
+ Uses the bleak library for cross-platform BLE support.
6
+
7
+ This enables wireless control of robots that support BLE serial
8
+ (ESP32, HM-10, nRF52, etc.) using the same driver code as USB serial.
9
+
10
+ Usage:
11
+ from ate.drivers.ble_transport import BLETransport
12
+
13
+ # Discover BLE devices
14
+ devices = await BLETransport.discover()
15
+
16
+ # Connect to a device
17
+ transport = BLETransport(
18
+ address="AA:BB:CC:DD:EE:FF",
19
+ service_uuid="0000ffe0-0000-1000-8000-00805f9b34fb",
20
+ char_uuid="0000ffe1-0000-1000-8000-00805f9b34fb",
21
+ )
22
+ await transport.connect()
23
+
24
+ # Use like serial
25
+ transport.write(b"command\\r\\n")
26
+ response = transport.read(100)
27
+
28
+ await transport.disconnect()
29
+ """
30
+
31
+ import asyncio
32
+ import queue
33
+ import threading
34
+ import time
35
+ from dataclasses import dataclass
36
+ from typing import Optional, List, Callable, Any
37
+
38
+ try:
39
+ from bleak import BleakClient, BleakScanner
40
+ from bleak.backends.device import BLEDevice
41
+ HAS_BLEAK = True
42
+ except ImportError:
43
+ HAS_BLEAK = False
44
+ BleakClient = None
45
+ BleakScanner = None
46
+ BLEDevice = None
47
+
48
+
49
+ @dataclass
50
+ class BLEDeviceInfo:
51
+ """Information about a discovered BLE device."""
52
+ address: str
53
+ name: str
54
+ rssi: int
55
+ services: List[str]
56
+
57
+ def __str__(self) -> str:
58
+ return f"{self.name or 'Unknown'} ({self.address}) RSSI: {self.rssi}"
59
+
60
+
61
+ class BLETransport:
62
+ """
63
+ BLE transport that provides a serial-compatible interface.
64
+
65
+ This allows robot drivers to use BLE with minimal changes.
66
+ The interface mimics pyserial's Serial class.
67
+ """
68
+
69
+ # Default ESP32/HM-10 BLE serial UUIDs
70
+ DEFAULT_SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb"
71
+ DEFAULT_WRITE_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb" # Write characteristic
72
+ DEFAULT_NOTIFY_CHAR_UUID = "0000ffe2-0000-1000-8000-00805f9b34fb" # Notify characteristic
73
+ # Legacy single-char UUID for devices that use same char for both
74
+ DEFAULT_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"
75
+
76
+ def __init__(
77
+ self,
78
+ address: str,
79
+ service_uuid: str = None,
80
+ char_uuid: str = None,
81
+ write_char_uuid: str = None,
82
+ notify_char_uuid: str = None,
83
+ timeout: float = 2.0,
84
+ ):
85
+ """
86
+ Initialize BLE transport.
87
+
88
+ Args:
89
+ address: BLE device address (MAC on Windows/Linux, UUID on macOS)
90
+ service_uuid: BLE service UUID (default: HM-10 compatible)
91
+ char_uuid: BLE characteristic UUID for read/write (legacy, single char)
92
+ write_char_uuid: BLE characteristic UUID for writing (ffe1)
93
+ notify_char_uuid: BLE characteristic UUID for notifications (ffe2)
94
+ timeout: Read timeout in seconds
95
+ """
96
+ if not HAS_BLEAK:
97
+ raise ImportError("bleak is required for BLE. Install with: pip install bleak")
98
+
99
+ self.address = address
100
+ self.service_uuid = service_uuid or self.DEFAULT_SERVICE_UUID
101
+
102
+ # Support both legacy single-char and separate write/notify chars
103
+ if write_char_uuid or notify_char_uuid:
104
+ self.write_char_uuid = write_char_uuid or self.DEFAULT_WRITE_CHAR_UUID
105
+ self.notify_char_uuid = notify_char_uuid or self.DEFAULT_NOTIFY_CHAR_UUID
106
+ elif char_uuid:
107
+ # Legacy: same char for both
108
+ self.write_char_uuid = char_uuid
109
+ self.notify_char_uuid = char_uuid
110
+ else:
111
+ # Default: use separate chars (MechDog style)
112
+ self.write_char_uuid = self.DEFAULT_WRITE_CHAR_UUID
113
+ self.notify_char_uuid = self.DEFAULT_NOTIFY_CHAR_UUID
114
+
115
+ # Keep char_uuid for backwards compatibility
116
+ self.char_uuid = self.write_char_uuid
117
+ self.timeout = timeout
118
+
119
+ self._client: Optional[BleakClient] = None
120
+ self._connected = False
121
+ self._rx_buffer = queue.Queue()
122
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
123
+ self._thread: Optional[threading.Thread] = None
124
+
125
+ @staticmethod
126
+ async def discover(
127
+ timeout: float = 10.0,
128
+ name_filter: str = None,
129
+ ) -> List[BLEDeviceInfo]:
130
+ """
131
+ Discover BLE devices.
132
+
133
+ Args:
134
+ timeout: Scan duration in seconds
135
+ name_filter: Only return devices whose name contains this string
136
+
137
+ Returns:
138
+ List of discovered BLE devices
139
+ """
140
+ if not HAS_BLEAK:
141
+ raise ImportError("bleak is required for BLE. Install with: pip install bleak")
142
+
143
+ devices = await BleakScanner.discover(timeout=timeout)
144
+
145
+ result = []
146
+ for d in devices:
147
+ if name_filter and (not d.name or name_filter.lower() not in d.name.lower()):
148
+ continue
149
+
150
+ info = BLEDeviceInfo(
151
+ address=d.address,
152
+ name=d.name or "Unknown",
153
+ rssi=d.rssi if hasattr(d, 'rssi') else -100,
154
+ services=[], # Would need to connect to enumerate
155
+ )
156
+ result.append(info)
157
+
158
+ # Sort by signal strength
159
+ result.sort(key=lambda x: x.rssi, reverse=True)
160
+ return result
161
+
162
+ @staticmethod
163
+ def discover_sync(timeout: float = 10.0, name_filter: str = None) -> List[BLEDeviceInfo]:
164
+ """Synchronous wrapper for discover()."""
165
+ return asyncio.run(BLETransport.discover(timeout, name_filter))
166
+
167
+ def _notification_handler(self, sender: int, data: bytearray):
168
+ """Handle incoming BLE notifications."""
169
+ self._rx_buffer.put(bytes(data))
170
+
171
+ async def _connect_async(self) -> bool:
172
+ """Async connect implementation."""
173
+ try:
174
+ self._client = BleakClient(self.address)
175
+ await self._client.connect()
176
+
177
+ # Subscribe to notifications on the NOTIFY characteristic (ffe2)
178
+ await self._client.start_notify(self.notify_char_uuid, self._notification_handler)
179
+
180
+ self._connected = True
181
+ return True
182
+ except Exception as e:
183
+ print(f"BLE connect error: {e}")
184
+ self._connected = False
185
+ return False
186
+
187
+ async def _disconnect_async(self):
188
+ """Async disconnect implementation."""
189
+ if self._client and self._connected:
190
+ try:
191
+ await self._client.stop_notify(self.notify_char_uuid)
192
+ await self._client.disconnect()
193
+ except Exception:
194
+ pass
195
+ self._connected = False
196
+ self._client = None
197
+
198
+ async def _write_async(self, data: bytes):
199
+ """Async write implementation."""
200
+ if not self._connected or not self._client:
201
+ raise IOError("Not connected")
202
+
203
+ # Split into chunks if needed (BLE has MTU limits)
204
+ chunk_size = 20 # Safe default, could be negotiated higher
205
+ for i in range(0, len(data), chunk_size):
206
+ chunk = data[i:i + chunk_size]
207
+ # Write to the WRITE characteristic (ffe1), without response for speed
208
+ await self._client.write_gatt_char(self.write_char_uuid, chunk, response=False)
209
+
210
+ def _run_async_loop(self):
211
+ """Run the async event loop in a background thread."""
212
+ self._loop = asyncio.new_event_loop()
213
+ asyncio.set_event_loop(self._loop)
214
+ self._loop.run_forever()
215
+
216
+ def _run_coroutine(self, coro) -> Any:
217
+ """Run a coroutine from sync code."""
218
+ if self._loop is None:
219
+ # Start the async loop in a background thread
220
+ self._thread = threading.Thread(target=self._run_async_loop, daemon=True)
221
+ self._thread.start()
222
+ time.sleep(0.1) # Give the loop time to start
223
+
224
+ future = asyncio.run_coroutine_threadsafe(coro, self._loop)
225
+ return future.result(timeout=self.timeout * 2)
226
+
227
+ def connect(self) -> bool:
228
+ """
229
+ Connect to the BLE device.
230
+
231
+ Returns:
232
+ True if connected successfully
233
+ """
234
+ return self._run_coroutine(self._connect_async())
235
+
236
+ def disconnect(self):
237
+ """Disconnect from the BLE device."""
238
+ if self._loop:
239
+ self._run_coroutine(self._disconnect_async())
240
+
241
+ if self._loop:
242
+ self._loop.call_soon_threadsafe(self._loop.stop)
243
+ if self._thread:
244
+ self._thread.join(timeout=1.0)
245
+
246
+ def write(self, data: bytes) -> int:
247
+ """
248
+ Write data to the BLE device.
249
+
250
+ Args:
251
+ data: Bytes to send
252
+
253
+ Returns:
254
+ Number of bytes written
255
+ """
256
+ self._run_coroutine(self._write_async(data))
257
+ return len(data)
258
+
259
+ def read(self, size: int = 1) -> bytes:
260
+ """
261
+ Read data from the BLE device.
262
+
263
+ Args:
264
+ size: Maximum bytes to read
265
+
266
+ Returns:
267
+ Received bytes (may be less than size)
268
+ """
269
+ result = b""
270
+ deadline = time.time() + self.timeout
271
+
272
+ while len(result) < size and time.time() < deadline:
273
+ try:
274
+ chunk = self._rx_buffer.get(timeout=0.1)
275
+ result += chunk
276
+ except queue.Empty:
277
+ if result: # Got some data, return it
278
+ break
279
+
280
+ return result[:size]
281
+
282
+ def read_until(self, terminator: bytes = b"\n") -> bytes:
283
+ """
284
+ Read until terminator is found.
285
+
286
+ Args:
287
+ terminator: Bytes to read until
288
+
289
+ Returns:
290
+ Data including terminator
291
+ """
292
+ result = b""
293
+ deadline = time.time() + self.timeout
294
+
295
+ while time.time() < deadline:
296
+ try:
297
+ chunk = self._rx_buffer.get(timeout=0.1)
298
+ result += chunk
299
+ if terminator in result:
300
+ break
301
+ except queue.Empty:
302
+ continue
303
+
304
+ return result
305
+
306
+ def readline(self) -> bytes:
307
+ """Read a line (until newline)."""
308
+ return self.read_until(b"\n")
309
+
310
+ def flush(self):
311
+ """Flush buffers (no-op for BLE)."""
312
+ pass
313
+
314
+ def reset_input_buffer(self):
315
+ """Clear the input buffer."""
316
+ while not self._rx_buffer.empty():
317
+ try:
318
+ self._rx_buffer.get_nowait()
319
+ except queue.Empty:
320
+ break
321
+
322
+ def reset_output_buffer(self):
323
+ """Clear the output buffer (no-op for BLE)."""
324
+ pass
325
+
326
+ @property
327
+ def is_open(self) -> bool:
328
+ """Check if connection is open."""
329
+ return self._connected
330
+
331
+ @property
332
+ def in_waiting(self) -> int:
333
+ """Number of bytes in input buffer."""
334
+ return self._rx_buffer.qsize() * 20 # Approximate
335
+
336
+ def close(self):
337
+ """Close the connection."""
338
+ self.disconnect()
339
+
340
+ def __enter__(self):
341
+ self.connect()
342
+ return self
343
+
344
+ def __exit__(self, *args):
345
+ self.close()
346
+
347
+
348
+ def discover_ble_robots(
349
+ timeout: float = 10.0,
350
+ name_patterns: List[str] = None,
351
+ ) -> List[BLEDeviceInfo]:
352
+ """
353
+ Discover BLE robots by scanning for known device names.
354
+
355
+ Args:
356
+ timeout: Scan duration
357
+ name_patterns: Device name patterns to match (e.g., ["MechDog", "ESP32"])
358
+
359
+ Returns:
360
+ List of discovered robot devices
361
+ """
362
+ if not HAS_BLEAK:
363
+ print("Warning: bleak not installed. Run: pip install bleak")
364
+ return []
365
+
366
+ if name_patterns is None:
367
+ name_patterns = [
368
+ "MechDog",
369
+ "HiWonder",
370
+ "ESP32",
371
+ "ESP-",
372
+ "BT",
373
+ "HC-",
374
+ "HM-",
375
+ ]
376
+
377
+ all_devices = BLETransport.discover_sync(timeout=timeout)
378
+
379
+ # Filter by name patterns
380
+ robots = []
381
+ for device in all_devices:
382
+ if not device.name:
383
+ continue
384
+ for pattern in name_patterns:
385
+ if pattern.lower() in device.name.lower():
386
+ robots.append(device)
387
+ break
388
+
389
+ return robots
390
+
391
+
392
+ if __name__ == "__main__":
393
+ # Test discovery
394
+ print("Scanning for BLE devices...")
395
+ devices = BLETransport.discover_sync(timeout=5.0)
396
+
397
+ print(f"\nFound {len(devices)} devices:")
398
+ for d in devices:
399
+ print(f" {d}")
400
+
401
+ # Filter for potential robots
402
+ print("\nPotential robots:")
403
+ robots = discover_ble_robots(timeout=5.0)
404
+ for r in robots:
405
+ print(f" {r}")