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
@@ -0,0 +1,121 @@
1
+ """
2
+ Transport Abstraction Layer for Robot Communication.
3
+
4
+ This module provides a unified interface for communicating with robots
5
+ across different physical transports (BLE, USB Serial, WiFi, etc.).
6
+
7
+ The key insight: Users shouldn't need to know HOW a robot communicates,
8
+ only WHAT they want it to do.
9
+
10
+ Example:
11
+ from ate.transports import connect
12
+
13
+ # Auto-discovers available transports
14
+ robot = await connect("mechdog")
15
+
16
+ # These work regardless of underlying transport
17
+ await robot.walk_forward()
18
+ await robot.gripper_open() # Automatically routes to USB if BLE can't do it
19
+ """
20
+
21
+ from typing import Optional
22
+
23
+ from .base import (
24
+ RobotTransport,
25
+ TransportCapability,
26
+ TransportState,
27
+ ConnectionInfo,
28
+ CommandResult,
29
+ HiWonderProtocol,
30
+ )
31
+ from .ble import BLETransport
32
+ from .serial import SerialTransport
33
+ from .hybrid import HybridTransport
34
+
35
+
36
+ async def connect(
37
+ address: Optional[str] = None,
38
+ name: Optional[str] = None,
39
+ transport_type: str = "auto"
40
+ ) -> HybridTransport:
41
+ """
42
+ Connect to a robot with automatic transport detection.
43
+
44
+ This is the main entry point for the transport layer.
45
+ It discovers available transports and creates a hybrid connection.
46
+
47
+ Args:
48
+ address: Specific address to connect to (BLE MAC/UUID or serial port)
49
+ name: Robot name hint for discovery (e.g., "mechdog")
50
+ transport_type: "auto", "ble", "serial", or "hybrid"
51
+
52
+ Returns:
53
+ Connected HybridTransport instance
54
+
55
+ Example:
56
+ # Auto-discover and connect
57
+ robot = await connect(name="mechdog")
58
+ await robot.walk_forward()
59
+
60
+ # Connect to specific BLE device
61
+ robot = await connect(address="FAF198B6-D9A4-CC27-7BBA-3159F63A61A9")
62
+
63
+ # Connect to specific serial port
64
+ robot = await connect(address="/dev/cu.usbmodem1234")
65
+ """
66
+ robot = HybridTransport()
67
+
68
+ if address:
69
+ # Direct address provided
70
+ await robot.connect(address, name=name)
71
+ else:
72
+ # Discover and connect to first matching device
73
+ connections = await HybridTransport.discover(timeout=5.0)
74
+
75
+ ble_device = None
76
+ serial_device = None
77
+
78
+ for conn in connections:
79
+ if name and conn.name and name.lower() not in conn.name.lower():
80
+ continue
81
+
82
+ if conn.transport_type == "ble" and not ble_device:
83
+ ble_device = conn
84
+ elif conn.transport_type == "serial" and not serial_device:
85
+ serial_device = conn
86
+
87
+ if ble_device:
88
+ await robot.connect_ble(ble_device.address)
89
+ if serial_device:
90
+ await robot.connect_serial(serial_device.address)
91
+
92
+ return robot
93
+
94
+
95
+ async def discover(timeout: float = 5.0) -> list[ConnectionInfo]:
96
+ """
97
+ Discover all available robots across all transports.
98
+
99
+ Returns:
100
+ List of discovered robot connections
101
+ """
102
+ return await HybridTransport.discover(timeout)
103
+
104
+
105
+ __all__ = [
106
+ # Core types
107
+ "RobotTransport",
108
+ "TransportCapability",
109
+ "TransportState",
110
+ "ConnectionInfo",
111
+ "CommandResult",
112
+ # Protocol adapters
113
+ "HiWonderProtocol",
114
+ # Transport implementations
115
+ "BLETransport",
116
+ "SerialTransport",
117
+ "HybridTransport",
118
+ # Convenience functions
119
+ "connect",
120
+ "discover",
121
+ ]
ate/transports/base.py ADDED
@@ -0,0 +1,394 @@
1
+ """
2
+ Base Transport Interface.
3
+
4
+ Defines the abstract interface that all robot transports must implement.
5
+ This allows the rest of the codebase to work with robots without knowing
6
+ the underlying communication protocol.
7
+ """
8
+
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum, auto
12
+ from typing import Optional, Callable, Awaitable, Any
13
+ import asyncio
14
+
15
+
16
+ class TransportState(Enum):
17
+ """Current state of a transport connection."""
18
+ DISCONNECTED = auto()
19
+ CONNECTING = auto()
20
+ CONNECTED = auto()
21
+ ERROR = auto()
22
+
23
+
24
+ class TransportCapability(Enum):
25
+ """
26
+ Capabilities that a transport may or may not support.
27
+
28
+ This is CRITICAL for the abstraction layer - we discovered that:
29
+ - BLE can do locomotion but NOT arm control on MechDog
30
+ - USB Serial can do arm control but connection is finicky
31
+ - Different firmware versions support different commands
32
+
33
+ By probing capabilities, we can automatically route commands
34
+ to the right transport.
35
+ """
36
+ # Locomotion
37
+ WALK = auto()
38
+ TURN = auto()
39
+ STAND = auto()
40
+ SIT = auto()
41
+ POSTURE = auto()
42
+
43
+ # Manipulation
44
+ ARM_POSITION = auto()
45
+ GRIPPER = auto()
46
+
47
+ # Sensors
48
+ ULTRASONIC = auto()
49
+ IMU = auto()
50
+ BATTERY = auto()
51
+
52
+ # Actions (pre-programmed sequences)
53
+ ACTION_GROUPS = auto()
54
+
55
+ # LED/Visual
56
+ RGB_LED = auto()
57
+
58
+ # Low-level
59
+ RAW_SERVO = auto()
60
+
61
+ # Meta
62
+ BIDIRECTIONAL = auto() # Can receive responses
63
+ STREAMING = auto() # Can stream continuous data
64
+
65
+
66
+ @dataclass
67
+ class ConnectionInfo:
68
+ """Information about a discovered or connected transport."""
69
+ transport_type: str # "ble", "serial", "wifi"
70
+ address: str # MAC address, port, or IP
71
+ name: Optional[str] = None
72
+ rssi: Optional[int] = None # Signal strength for wireless
73
+ metadata: dict = field(default_factory=dict)
74
+
75
+
76
+ @dataclass
77
+ class CommandResult:
78
+ """Result of sending a command."""
79
+ success: bool
80
+ response: Optional[str] = None
81
+ raw_data: Optional[bytes] = None
82
+ error: Optional[str] = None
83
+ latency_ms: Optional[float] = None
84
+
85
+
86
+ class RobotTransport(ABC):
87
+ """
88
+ Abstract base class for robot communication transports.
89
+
90
+ Every transport (BLE, Serial, WiFi) implements this interface,
91
+ allowing higher-level code to work with any robot without
92
+ caring about the underlying protocol.
93
+
94
+ Key design decisions:
95
+ 1. Async-first: Robot communication is inherently async
96
+ 2. Capability-based: Not all transports support all features
97
+ 3. Response handling: Can register callbacks for async responses
98
+ 4. Connection lifecycle: Connect/disconnect/reconnect handling
99
+ """
100
+
101
+ def __init__(self):
102
+ self._state = TransportState.DISCONNECTED
103
+ self._capabilities: set[TransportCapability] = set()
104
+ self._response_handlers: list[Callable[[bytes], Awaitable[None]]] = []
105
+ self._connection_info: Optional[ConnectionInfo] = None
106
+
107
+ @property
108
+ def state(self) -> TransportState:
109
+ """Current connection state."""
110
+ return self._state
111
+
112
+ @property
113
+ def is_connected(self) -> bool:
114
+ """Whether transport is currently connected."""
115
+ return self._state == TransportState.CONNECTED
116
+
117
+ @property
118
+ def capabilities(self) -> set[TransportCapability]:
119
+ """Set of capabilities this transport supports."""
120
+ return self._capabilities.copy()
121
+
122
+ @property
123
+ def connection_info(self) -> Optional[ConnectionInfo]:
124
+ """Information about the current connection."""
125
+ return self._connection_info
126
+
127
+ def has_capability(self, cap: TransportCapability) -> bool:
128
+ """Check if this transport supports a specific capability."""
129
+ return cap in self._capabilities
130
+
131
+ # ========== Abstract Methods ==========
132
+
133
+ @abstractmethod
134
+ async def connect(self, address: str, **kwargs) -> bool:
135
+ """
136
+ Connect to a robot at the given address.
137
+
138
+ Args:
139
+ address: Transport-specific address (MAC, port, IP)
140
+ **kwargs: Transport-specific options
141
+
142
+ Returns:
143
+ True if connection successful
144
+ """
145
+ pass
146
+
147
+ @abstractmethod
148
+ async def disconnect(self) -> None:
149
+ """Disconnect from the robot."""
150
+ pass
151
+
152
+ @abstractmethod
153
+ async def send(self, data: bytes) -> CommandResult:
154
+ """
155
+ Send raw data to the robot.
156
+
157
+ Args:
158
+ data: Raw bytes to send
159
+
160
+ Returns:
161
+ CommandResult with success status and any response
162
+ """
163
+ pass
164
+
165
+ @abstractmethod
166
+ async def send_command(self, command: str) -> CommandResult:
167
+ """
168
+ Send a formatted command string to the robot.
169
+
170
+ This handles protocol-specific formatting (e.g., HiWonder CMD|...|$)
171
+
172
+ Args:
173
+ command: Command string in transport's native format
174
+
175
+ Returns:
176
+ CommandResult with success status and any response
177
+ """
178
+ pass
179
+
180
+ @classmethod
181
+ @abstractmethod
182
+ async def discover(cls, timeout: float = 5.0) -> list[ConnectionInfo]:
183
+ """
184
+ Discover available robots using this transport.
185
+
186
+ Args:
187
+ timeout: How long to scan in seconds
188
+
189
+ Returns:
190
+ List of discovered connection info
191
+ """
192
+ pass
193
+
194
+ @abstractmethod
195
+ async def probe_capabilities(self) -> set[TransportCapability]:
196
+ """
197
+ Probe the connected robot to discover what it can do.
198
+
199
+ This is where we send test commands and see what works.
200
+ Essential for handling firmware fragmentation.
201
+
202
+ Returns:
203
+ Set of capabilities that work on this connection
204
+ """
205
+ pass
206
+
207
+ # ========== Response Handling ==========
208
+
209
+ def add_response_handler(
210
+ self,
211
+ handler: Callable[[bytes], Awaitable[None]]
212
+ ) -> None:
213
+ """Register a callback for async responses from robot."""
214
+ self._response_handlers.append(handler)
215
+
216
+ def remove_response_handler(
217
+ self,
218
+ handler: Callable[[bytes], Awaitable[None]]
219
+ ) -> None:
220
+ """Remove a response handler."""
221
+ if handler in self._response_handlers:
222
+ self._response_handlers.remove(handler)
223
+
224
+ async def _notify_handlers(self, data: bytes) -> None:
225
+ """Notify all registered handlers of received data."""
226
+ for handler in self._response_handlers:
227
+ try:
228
+ await handler(data)
229
+ except Exception as e:
230
+ # Don't let one handler crash others
231
+ print(f"Handler error: {e}")
232
+
233
+ # ========== Context Manager ==========
234
+
235
+ async def __aenter__(self):
236
+ """Async context manager entry."""
237
+ return self
238
+
239
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
240
+ """Async context manager exit - ensures cleanup."""
241
+ await self.disconnect()
242
+ return False
243
+
244
+
245
+ class ProtocolAdapter(ABC):
246
+ """
247
+ Adapts high-level commands to protocol-specific format.
248
+
249
+ Different robots use different command formats:
250
+ - HiWonder: CMD|function|sub|data|$
251
+ - Unitree: Binary protocol
252
+ - Some: JSON over WebSocket
253
+
254
+ The adapter translates between canonical commands and
255
+ protocol-specific formats.
256
+ """
257
+
258
+ @abstractmethod
259
+ def encode_command(self, command: str, *args) -> bytes:
260
+ """
261
+ Encode a canonical command into protocol-specific bytes.
262
+
263
+ Args:
264
+ command: Canonical command name (e.g., "walk_forward")
265
+ *args: Command arguments
266
+
267
+ Returns:
268
+ Protocol-specific bytes to send
269
+ """
270
+ pass
271
+
272
+ @abstractmethod
273
+ def decode_response(self, data: bytes) -> dict[str, Any]:
274
+ """
275
+ Decode protocol-specific response into canonical format.
276
+
277
+ Args:
278
+ data: Raw bytes received from robot
279
+
280
+ Returns:
281
+ Decoded response as a dictionary
282
+ """
283
+ pass
284
+
285
+
286
+ class HiWonderProtocol(ProtocolAdapter):
287
+ """
288
+ Protocol adapter for HiWonder robots (MechDog, etc).
289
+
290
+ Command format: CMD|function|subfunction|data...|$
291
+ Response format: CMD|function|data...|$
292
+ """
293
+
294
+ # Canonical command -> (function, subfunction, args_formatter)
295
+ COMMAND_MAP = {
296
+ # Posture commands (Function 1)
297
+ "posture_normal": (1, 0, None),
298
+ "posture_high": (1, 1, None),
299
+ "posture_low": (1, 2, None),
300
+ "turn_left": (1, 3, None),
301
+ "turn_right": (1, 4, None),
302
+ "posture_reset": (1, 5, None),
303
+ "sit": (1, 7, None),
304
+ "lie_down": (1, 8, None),
305
+ "low_crawl": (1, 9, None),
306
+ "high_crawl": (1, 10, None),
307
+ "twist": (1, 11, None),
308
+ "wave": (1, 12, None),
309
+ "handshake": (1, 13, None),
310
+
311
+ # Action groups (Function 2)
312
+ "action_group": (2, 1, lambda group_id: [group_id]),
313
+
314
+ # Movement (Function 3)
315
+ "move": (3, None, lambda x, y, yaw: [x, y, yaw]),
316
+ "walk_forward": (3, None, lambda: [100, 0, 0]),
317
+ "walk_backward": (3, None, lambda: [-100, 0, 0]),
318
+ "strafe_left": (3, None, lambda: [0, -100, 0]),
319
+ "strafe_right": (3, None, lambda: [0, 100, 0]),
320
+ "rotate_left": (3, None, lambda: [0, 0, 50]),
321
+ "rotate_right": (3, None, lambda: [0, 0, -50]),
322
+ "stop": (3, None, lambda: [0, 0, 0]),
323
+
324
+ # Sensors (Function 4)
325
+ "ultrasonic_query": (4, 1, None),
326
+ "rgb_led": (4, 3, lambda r, g, b: [r, g, b]),
327
+
328
+ # Battery (Function 6)
329
+ "battery_query": (6, None, None),
330
+
331
+ # Arm control (Function 7) - May not work on all firmware
332
+ "arm_action_1": (7, 1, None),
333
+ "arm_action_2": (7, 2, None),
334
+ "arm_joint_a_plus": (7, 3, lambda: [1]),
335
+ "arm_joint_a_minus": (7, 3, lambda: [-1]),
336
+ "arm_joint_b_plus": (7, 4, lambda: [1]),
337
+ "arm_joint_b_minus": (7, 4, lambda: [-1]),
338
+ "gripper_open": (7, 6, None),
339
+ "gripper_close": (7, 7, None),
340
+ }
341
+
342
+ def encode_command(self, command: str, *args) -> bytes:
343
+ """Encode canonical command to HiWonder format."""
344
+ if command not in self.COMMAND_MAP:
345
+ raise ValueError(f"Unknown command: {command}")
346
+
347
+ func, sub, args_formatter = self.COMMAND_MAP[command]
348
+
349
+ parts = ["CMD", str(func)]
350
+
351
+ if sub is not None:
352
+ parts.append(str(sub))
353
+
354
+ if args_formatter:
355
+ formatted_args = args_formatter(*args) if args else args_formatter()
356
+ parts.extend(str(a) for a in formatted_args)
357
+
358
+ parts.append("$")
359
+ return "|".join(parts).encode('ascii')
360
+
361
+ def decode_response(self, data: bytes) -> dict[str, Any]:
362
+ """Decode HiWonder response."""
363
+ try:
364
+ text = data.decode('ascii').strip()
365
+ if not text.startswith("CMD|"):
366
+ return {"raw": text}
367
+
368
+ parts = text.split("|")
369
+ func = int(parts[1])
370
+
371
+ result = {
372
+ "function": func,
373
+ "raw": text,
374
+ "parts": parts,
375
+ }
376
+
377
+ # Parse known response types
378
+ if func == 4 and len(parts) >= 3:
379
+ # Ultrasonic response: CMD|4|<mm>|$
380
+ try:
381
+ result["ultrasonic_mm"] = int(parts[2])
382
+ except ValueError:
383
+ pass
384
+ elif func == 6 and len(parts) >= 3:
385
+ # Battery response: CMD|6|<voltage>|$ (voltage in 0.01V)
386
+ try:
387
+ result["battery_voltage"] = float(parts[2])
388
+ except ValueError:
389
+ pass
390
+
391
+ return result
392
+
393
+ except Exception as e:
394
+ return {"error": str(e), "raw_bytes": data}