foodforthought-cli 0.2.8__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 (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 +399 -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.0.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.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.0.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,444 @@
1
+ """
2
+ Hybrid Transport Implementation.
3
+
4
+ Combines multiple transports (BLE + Serial) to provide unified access
5
+ to all robot capabilities. This is the key abstraction that solves
6
+ the "different capabilities on different transports" problem.
7
+
8
+ Example: MechDog
9
+ - BLE: Locomotion (walk, turn, stand), sensors (ultrasonic, battery)
10
+ - USB Serial: Arm control (MicroPython REPL), gripper, raw servo access
11
+
12
+ The hybrid transport automatically routes commands to the right transport.
13
+ """
14
+
15
+ import asyncio
16
+ from typing import Optional
17
+ from dataclasses import dataclass, field
18
+
19
+ from .base import (
20
+ RobotTransport,
21
+ TransportState,
22
+ TransportCapability,
23
+ ConnectionInfo,
24
+ CommandResult,
25
+ )
26
+ from .ble import BLETransport
27
+ from .serial import SerialTransport
28
+
29
+
30
+ @dataclass
31
+ class HybridConfig:
32
+ """Configuration for hybrid transport."""
33
+ prefer_ble: bool = True # Prefer BLE when both support a capability
34
+ auto_connect_serial: bool = False # Auto-connect serial if BLE connected
35
+ parallel_discovery: bool = True
36
+
37
+
38
+ # Mapping of capabilities to preferred transport
39
+ CAPABILITY_TRANSPORT_MAP = {
40
+ # BLE is preferred for these (wireless, lower latency for movement)
41
+ TransportCapability.WALK: "ble",
42
+ TransportCapability.TURN: "ble",
43
+ TransportCapability.STAND: "ble",
44
+ TransportCapability.SIT: "ble",
45
+ TransportCapability.POSTURE: "ble",
46
+ TransportCapability.ACTION_GROUPS: "ble",
47
+ TransportCapability.ULTRASONIC: "ble",
48
+ TransportCapability.BATTERY: "ble",
49
+ TransportCapability.RGB_LED: "ble",
50
+
51
+ # Serial is REQUIRED for these (BLE can't do them on MechDog)
52
+ TransportCapability.ARM_POSITION: "serial",
53
+ TransportCapability.GRIPPER: "serial",
54
+ TransportCapability.RAW_SERVO: "serial",
55
+ TransportCapability.IMU: "serial",
56
+ }
57
+
58
+
59
+ class HybridTransport(RobotTransport):
60
+ """
61
+ Hybrid transport that combines BLE and Serial connections.
62
+
63
+ Automatically routes commands to the appropriate transport based on:
64
+ 1. Required capability for the command
65
+ 2. Which transports have that capability
66
+ 3. User preference (prefer_ble config)
67
+
68
+ Example:
69
+ async with HybridTransport() as robot:
70
+ # Connect to both transports
71
+ await robot.connect_all(
72
+ ble_address="FAF198B6-...",
73
+ serial_port="/dev/cu.usbmodem1234"
74
+ )
75
+
76
+ # These automatically route to the right transport
77
+ await robot.walk_forward() # -> BLE
78
+ await robot.gripper_open() # -> Serial
79
+ await robot.get_battery() # -> BLE
80
+ await robot.arm_move([(1, 90)]) # -> Serial
81
+ """
82
+
83
+ def __init__(self, config: Optional[HybridConfig] = None):
84
+ super().__init__()
85
+
86
+ self.config = config or HybridConfig()
87
+ self._ble: Optional[BLETransport] = None
88
+ self._serial: Optional[SerialTransport] = None
89
+
90
+ @property
91
+ def ble(self) -> Optional[BLETransport]:
92
+ """Access underlying BLE transport."""
93
+ return self._ble
94
+
95
+ @property
96
+ def serial_transport(self) -> Optional[SerialTransport]:
97
+ """Access underlying serial transport."""
98
+ return self._serial
99
+
100
+ @property
101
+ def is_connected(self) -> bool:
102
+ """True if at least one transport is connected."""
103
+ ble_connected = self._ble and self._ble.is_connected
104
+ serial_connected = self._serial and self._serial.is_connected
105
+ return ble_connected or serial_connected
106
+
107
+ async def connect(self, address: str, **kwargs) -> bool:
108
+ """
109
+ Connect using the appropriate transport based on address format.
110
+
111
+ Args:
112
+ address: Either BLE address (UUID/MAC) or serial port path
113
+ """
114
+ if address.startswith("/dev") or address.startswith("COM"):
115
+ return await self.connect_serial(address, **kwargs)
116
+ else:
117
+ return await self.connect_ble(address, **kwargs)
118
+
119
+ async def connect_ble(self, address: str, **kwargs) -> bool:
120
+ """Connect BLE transport."""
121
+ self._ble = BLETransport()
122
+ success = await self._ble.connect(address, **kwargs)
123
+
124
+ if success:
125
+ self._state = TransportState.CONNECTED
126
+ self._update_capabilities()
127
+
128
+ return success
129
+
130
+ async def connect_serial(self, port: str, **kwargs) -> bool:
131
+ """Connect serial transport."""
132
+ self._serial = SerialTransport()
133
+ success = await self._serial.connect(port, **kwargs)
134
+
135
+ if success:
136
+ self._state = TransportState.CONNECTED
137
+ self._update_capabilities()
138
+
139
+ return success
140
+
141
+ async def connect_all(
142
+ self,
143
+ ble_address: Optional[str] = None,
144
+ serial_port: Optional[str] = None,
145
+ **kwargs
146
+ ) -> dict[str, bool]:
147
+ """
148
+ Connect to both transports.
149
+
150
+ Args:
151
+ ble_address: BLE device address
152
+ serial_port: Serial port path
153
+
154
+ Returns:
155
+ Dict with connection status for each transport
156
+ """
157
+ results = {}
158
+
159
+ if ble_address:
160
+ results["ble"] = await self.connect_ble(ble_address, **kwargs)
161
+
162
+ if serial_port:
163
+ results["serial"] = await self.connect_serial(serial_port, **kwargs)
164
+
165
+ return results
166
+
167
+ async def disconnect(self) -> None:
168
+ """Disconnect all transports."""
169
+ if self._ble:
170
+ await self._ble.disconnect()
171
+ self._ble = None
172
+
173
+ if self._serial:
174
+ await self._serial.disconnect()
175
+ self._serial = None
176
+
177
+ self._state = TransportState.DISCONNECTED
178
+
179
+ async def send(self, data: bytes) -> CommandResult:
180
+ """
181
+ Send raw data using BLE transport (preferred for raw commands).
182
+
183
+ For capability-specific commands, use the high-level methods instead.
184
+ """
185
+ if self._ble and self._ble.is_connected:
186
+ return await self._ble.send(data)
187
+ elif self._serial and self._serial.is_connected:
188
+ return await self._serial.send(data)
189
+ else:
190
+ return CommandResult(success=False, error="No transport connected")
191
+
192
+ async def send_command(self, command: str) -> CommandResult:
193
+ """
194
+ Send a command string, routing to appropriate transport.
195
+
196
+ Tries to infer the right transport from command format.
197
+ """
198
+ # HiWonder BLE commands start with "CMD|"
199
+ if command.startswith("CMD|") and self._ble and self._ble.is_connected:
200
+ return await self._ble.send_command(command)
201
+
202
+ # Python-like commands go to serial REPL
203
+ if self._serial and self._serial.is_connected:
204
+ return await self._serial.send_command(command)
205
+
206
+ # Fall back to BLE
207
+ if self._ble and self._ble.is_connected:
208
+ return await self._ble.send_command(command)
209
+
210
+ return CommandResult(success=False, error="No transport connected")
211
+
212
+ @classmethod
213
+ async def discover(cls, timeout: float = 5.0) -> list[ConnectionInfo]:
214
+ """Discover all available robots across all transports."""
215
+ results = []
216
+
217
+ # Discover in parallel
218
+ ble_task = asyncio.create_task(BLETransport.discover(timeout))
219
+ serial_task = asyncio.create_task(SerialTransport.discover(timeout))
220
+
221
+ ble_results, serial_results = await asyncio.gather(
222
+ ble_task, serial_task,
223
+ return_exceptions=True
224
+ )
225
+
226
+ if isinstance(ble_results, list):
227
+ results.extend(ble_results)
228
+ if isinstance(serial_results, list):
229
+ results.extend(serial_results)
230
+
231
+ return results
232
+
233
+ async def probe_capabilities(self) -> set[TransportCapability]:
234
+ """Probe capabilities from all connected transports."""
235
+ caps = set()
236
+
237
+ if self._ble and self._ble.is_connected:
238
+ caps.update(await self._ble.probe_capabilities())
239
+
240
+ if self._serial and self._serial.is_connected:
241
+ caps.update(await self._serial.probe_capabilities())
242
+
243
+ self._capabilities = caps
244
+ return caps
245
+
246
+ def _update_capabilities(self) -> None:
247
+ """Update combined capabilities from all transports."""
248
+ caps = set()
249
+
250
+ if self._ble:
251
+ caps.update(self._ble.capabilities)
252
+
253
+ if self._serial:
254
+ caps.update(self._serial.capabilities)
255
+
256
+ self._capabilities = caps
257
+
258
+ def _get_transport_for_capability(
259
+ self,
260
+ cap: TransportCapability
261
+ ) -> Optional[RobotTransport]:
262
+ """Get the appropriate transport for a capability."""
263
+ preferred = CAPABILITY_TRANSPORT_MAP.get(cap)
264
+
265
+ # Check preferred transport first
266
+ if preferred == "ble" and self._ble and self._ble.has_capability(cap):
267
+ return self._ble
268
+ if preferred == "serial" and self._serial and self._serial.has_capability(cap):
269
+ return self._serial
270
+
271
+ # Fall back to any transport that has the capability
272
+ if self._ble and self._ble.has_capability(cap):
273
+ return self._ble
274
+ if self._serial and self._serial.has_capability(cap):
275
+ return self._serial
276
+
277
+ return None
278
+
279
+ # ========== High-Level Robot Commands ==========
280
+ # These automatically route to the correct transport
281
+
282
+ async def walk_forward(self, speed: int = 100) -> CommandResult:
283
+ """Walk forward - routes to BLE."""
284
+ if self._ble and self._ble.is_connected:
285
+ return await self._ble.walk_forward(speed)
286
+ return CommandResult(success=False, error="BLE not connected")
287
+
288
+ async def walk_backward(self, speed: int = 100) -> CommandResult:
289
+ """Walk backward - routes to BLE."""
290
+ if self._ble and self._ble.is_connected:
291
+ return await self._ble.walk_backward(speed)
292
+ return CommandResult(success=False, error="BLE not connected")
293
+
294
+ async def stop(self) -> CommandResult:
295
+ """Stop movement - routes to BLE."""
296
+ if self._ble and self._ble.is_connected:
297
+ return await self._ble.stop()
298
+ return CommandResult(success=False, error="BLE not connected")
299
+
300
+ async def stand(self) -> CommandResult:
301
+ """Stand up - routes to BLE."""
302
+ if self._ble and self._ble.is_connected:
303
+ return await self._ble.stand()
304
+ return CommandResult(success=False, error="BLE not connected")
305
+
306
+ async def sit(self) -> CommandResult:
307
+ """Sit down - routes to BLE."""
308
+ if self._ble and self._ble.is_connected:
309
+ return await self._ble.sit()
310
+ return CommandResult(success=False, error="BLE not connected")
311
+
312
+ async def get_battery(self) -> Optional[float]:
313
+ """Get battery voltage - routes to BLE."""
314
+ if self._ble and self._ble.is_connected:
315
+ return await self._ble.get_battery()
316
+ return None
317
+
318
+ async def get_ultrasonic(self) -> Optional[int]:
319
+ """Get ultrasonic distance - routes to BLE."""
320
+ if self._ble and self._ble.is_connected:
321
+ return await self._ble.get_ultrasonic()
322
+ return None
323
+
324
+ async def run_action_group(self, group_id: int) -> CommandResult:
325
+ """Run action group - routes to BLE."""
326
+ if self._ble and self._ble.is_connected:
327
+ return await self._ble.run_action_group(group_id)
328
+ return CommandResult(success=False, error="BLE not connected")
329
+
330
+ async def arm_move(self, positions: list[tuple[int, int]]) -> CommandResult:
331
+ """Move arm servos - routes to Serial."""
332
+ if self._serial and self._serial.is_connected:
333
+ return await self._serial.arm_move(positions)
334
+ return CommandResult(
335
+ success=False,
336
+ error="Serial not connected (arm control requires USB)"
337
+ )
338
+
339
+ async def gripper_open(self) -> CommandResult:
340
+ """Open gripper - routes to Serial."""
341
+ if self._serial and self._serial.is_connected:
342
+ return await self._serial.gripper_open()
343
+ return CommandResult(
344
+ success=False,
345
+ error="Serial not connected (gripper requires USB)"
346
+ )
347
+
348
+ async def gripper_close(self) -> CommandResult:
349
+ """Close gripper - routes to Serial."""
350
+ if self._serial and self._serial.is_connected:
351
+ return await self._serial.gripper_close()
352
+ return CommandResult(
353
+ success=False,
354
+ error="Serial not connected (gripper requires USB)"
355
+ )
356
+
357
+ async def execute_python(self, code: str) -> CommandResult:
358
+ """Execute Python code on robot - routes to Serial."""
359
+ if self._serial and self._serial.is_connected:
360
+ return await self._serial.execute_python(code)
361
+ return CommandResult(
362
+ success=False,
363
+ error="Serial not connected (Python execution requires USB)"
364
+ )
365
+
366
+ # ========== Ball Pickup Sequence ==========
367
+ # This demonstrates the power of hybrid transport
368
+
369
+ async def ball_pickup_sequence(
370
+ self,
371
+ approach_speed: int = 80,
372
+ max_distance_mm: int = 300
373
+ ) -> bool:
374
+ """
375
+ Execute ball pickup using both transports.
376
+
377
+ This is the canonical example of why hybrid transport matters:
378
+ 1. Use BLE ultrasonic to detect ball
379
+ 2. Use BLE locomotion to approach
380
+ 3. Use Serial to control gripper
381
+ 4. Use BLE to walk back
382
+
383
+ Returns:
384
+ True if pickup was successful
385
+ """
386
+ # Check we have both transports
387
+ if not (self._ble and self._ble.is_connected):
388
+ print("ERROR: BLE not connected (needed for locomotion/sensors)")
389
+ return False
390
+
391
+ if not (self._serial and self._serial.is_connected):
392
+ print("WARNING: Serial not connected (gripper may not work)")
393
+
394
+ print("=== Ball Pickup Sequence ===")
395
+
396
+ # Step 1: Detect ball with ultrasonic
397
+ print("1. Scanning for ball...")
398
+ distance = await self.get_ultrasonic()
399
+ if distance is None:
400
+ print(" ERROR: Ultrasonic not responding")
401
+ return False
402
+
403
+ print(f" Distance: {distance}mm")
404
+ if distance > max_distance_mm:
405
+ print(f" Ball too far (max {max_distance_mm}mm)")
406
+ return False
407
+
408
+ # Step 2: Approach
409
+ print("2. Approaching ball...")
410
+ while distance > 100: # Stop when close
411
+ await self.walk_forward(approach_speed)
412
+ await asyncio.sleep(0.5)
413
+ distance = await self.get_ultrasonic() or distance
414
+ print(f" Distance: {distance}mm")
415
+
416
+ await self.stop()
417
+ print(" Stopped at pickup position")
418
+
419
+ # Step 3: Lower and grab (uses Serial for gripper)
420
+ print("3. Grabbing ball...")
421
+ if self._serial and self._serial.is_connected:
422
+ await self.gripper_open()
423
+ await asyncio.sleep(0.5)
424
+ # Lower arm (example servo positions)
425
+ await self.arm_move([(1, 45), (2, 90), (3, 45)])
426
+ await asyncio.sleep(0.5)
427
+ await self.gripper_close()
428
+ await asyncio.sleep(0.5)
429
+ # Raise arm
430
+ await self.arm_move([(1, 90), (2, 45), (3, 90)])
431
+ print(" Grabbed!")
432
+ else:
433
+ print(" SKIP: No serial connection for gripper")
434
+
435
+ # Step 4: Walk back
436
+ print("4. Returning...")
437
+ for _ in range(5):
438
+ await self.walk_backward(approach_speed)
439
+ await asyncio.sleep(0.5)
440
+
441
+ await self.stop()
442
+ await self.stand()
443
+ print("=== Pickup Complete ===")
444
+ return True