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,345 @@
1
+ """
2
+ Serial Transport Implementation.
3
+
4
+ Uses pyserial for USB serial communication with robots.
5
+ This is needed for capabilities that BLE can't handle (like arm control
6
+ on MechDog, which requires MicroPython REPL access).
7
+
8
+ Key learnings:
9
+ - MechDog exposes MicroPython REPL over USB-C serial
10
+ - ARM control uses servo library: `arm.arm_move([(id, angle), ...])`
11
+ - Gripper is typically servo ID 10-12 depending on mounting
12
+ - Must handle REPL prompts (>>>) and async output
13
+ """
14
+
15
+ import asyncio
16
+ from typing import Optional
17
+ from dataclasses import dataclass
18
+ import re
19
+
20
+ try:
21
+ import serial
22
+ import serial.tools.list_ports
23
+ SERIAL_AVAILABLE = True
24
+ except ImportError:
25
+ SERIAL_AVAILABLE = False
26
+ serial = None
27
+
28
+ from .base import (
29
+ RobotTransport,
30
+ TransportState,
31
+ TransportCapability,
32
+ ConnectionInfo,
33
+ CommandResult,
34
+ )
35
+
36
+
37
+ @dataclass
38
+ class SerialConfig:
39
+ """Configuration for serial transport."""
40
+ baudrate: int = 115200
41
+ timeout: float = 1.0
42
+ write_timeout: float = 1.0
43
+ response_wait: float = 0.5
44
+ repl_prompt: str = ">>>"
45
+
46
+
47
+ class SerialTransport(RobotTransport):
48
+ """
49
+ USB Serial transport for robot communication.
50
+
51
+ Handles both raw serial protocols and MicroPython REPL connections.
52
+
53
+ Example:
54
+ async with SerialTransport() as transport:
55
+ if await transport.connect("/dev/cu.usbmodem1234"):
56
+ # MicroPython command
57
+ result = await transport.send_command(
58
+ "arm.arm_move([(1, 90), (2, 45)])"
59
+ )
60
+ """
61
+
62
+ def __init__(self, config: Optional[SerialConfig] = None):
63
+ super().__init__()
64
+
65
+ if not SERIAL_AVAILABLE:
66
+ raise ImportError(
67
+ "pyserial is required for serial transport. "
68
+ "Install with: pip install pyserial"
69
+ )
70
+
71
+ self.config = config or SerialConfig()
72
+ self._serial: Optional[serial.Serial] = None
73
+ self._read_task: Optional[asyncio.Task] = None
74
+ self._response_buffer: bytes = b""
75
+ self._is_repl = False
76
+
77
+ async def connect(self, address: str, **kwargs) -> bool:
78
+ """
79
+ Connect to a serial port.
80
+
81
+ Args:
82
+ address: Serial port path (e.g., /dev/cu.usbmodem1234)
83
+ **kwargs: baudrate, timeout, repl_mode
84
+
85
+ Returns:
86
+ True if connected successfully
87
+ """
88
+ self._state = TransportState.CONNECTING
89
+
90
+ try:
91
+ baudrate = kwargs.get("baudrate", self.config.baudrate)
92
+
93
+ self._serial = serial.Serial(
94
+ port=address,
95
+ baudrate=baudrate,
96
+ timeout=self.config.timeout,
97
+ write_timeout=self.config.write_timeout,
98
+ )
99
+
100
+ if not self._serial.is_open:
101
+ self._serial.open()
102
+
103
+ # Clear any pending data
104
+ self._serial.reset_input_buffer()
105
+ self._serial.reset_output_buffer()
106
+
107
+ # Check if this is a REPL
108
+ self._is_repl = await self._detect_repl()
109
+
110
+ self._state = TransportState.CONNECTED
111
+ self._connection_info = ConnectionInfo(
112
+ transport_type="serial",
113
+ address=address,
114
+ name=kwargs.get("name", "USB Serial"),
115
+ metadata={
116
+ "baudrate": baudrate,
117
+ "is_repl": self._is_repl,
118
+ }
119
+ )
120
+
121
+ # Start background reader
122
+ self._read_task = asyncio.create_task(self._read_loop())
123
+
124
+ return True
125
+
126
+ except Exception as e:
127
+ print(f"Serial connect error: {e}")
128
+ self._state = TransportState.ERROR
129
+ return False
130
+
131
+ async def disconnect(self) -> None:
132
+ """Disconnect from serial port."""
133
+ if self._read_task:
134
+ self._read_task.cancel()
135
+ try:
136
+ await self._read_task
137
+ except asyncio.CancelledError:
138
+ pass
139
+ self._read_task = None
140
+
141
+ if self._serial and self._serial.is_open:
142
+ self._serial.close()
143
+
144
+ self._serial = None
145
+ self._state = TransportState.DISCONNECTED
146
+ self._connection_info = None
147
+
148
+ async def send(self, data: bytes) -> CommandResult:
149
+ """Send raw bytes to the robot."""
150
+ if not self.is_connected or not self._serial:
151
+ return CommandResult(success=False, error="Not connected")
152
+
153
+ import time
154
+ start = time.perf_counter()
155
+
156
+ try:
157
+ # Clear buffer
158
+ self._response_buffer = b""
159
+ self._serial.reset_input_buffer()
160
+
161
+ # Send data
162
+ self._serial.write(data)
163
+ self._serial.flush()
164
+
165
+ # Wait for response
166
+ await asyncio.sleep(self.config.response_wait)
167
+
168
+ # Read response
169
+ response = b""
170
+ while self._serial.in_waiting > 0:
171
+ response += self._serial.read(self._serial.in_waiting)
172
+ await asyncio.sleep(0.05)
173
+
174
+ latency = (time.perf_counter() - start) * 1000
175
+
176
+ return CommandResult(
177
+ success=True,
178
+ response=response.decode('utf-8', errors='replace'),
179
+ raw_data=response,
180
+ latency_ms=latency
181
+ )
182
+
183
+ except Exception as e:
184
+ return CommandResult(success=False, error=str(e))
185
+
186
+ async def send_command(self, command: str) -> CommandResult:
187
+ """
188
+ Send a command string.
189
+
190
+ If connected to REPL, wraps appropriately.
191
+ """
192
+ if self._is_repl:
193
+ # MicroPython REPL - add newline
194
+ data = (command.strip() + "\r\n").encode('utf-8')
195
+ else:
196
+ data = command.encode('utf-8')
197
+
198
+ return await self.send(data)
199
+
200
+ @classmethod
201
+ async def discover(cls, timeout: float = 5.0) -> list[ConnectionInfo]:
202
+ """Discover available serial ports."""
203
+ if not SERIAL_AVAILABLE:
204
+ return []
205
+
206
+ ports = serial.tools.list_ports.comports()
207
+ results = []
208
+
209
+ for port in ports:
210
+ # Filter for likely robot connections
211
+ # MechDog typically appears as "USB Modem" or similar
212
+ desc = (port.description or "").lower()
213
+ is_robot = any(kw in desc for kw in [
214
+ "modem", "serial", "uart", "esp32", "micropython",
215
+ "ch340", "cp210", "ftdi"
216
+ ])
217
+
218
+ if is_robot or port.vid: # Has vendor ID = USB device
219
+ results.append(ConnectionInfo(
220
+ transport_type="serial",
221
+ address=port.device,
222
+ name=port.description,
223
+ metadata={
224
+ "vid": port.vid,
225
+ "pid": port.pid,
226
+ "serial_number": port.serial_number,
227
+ }
228
+ ))
229
+
230
+ return results
231
+
232
+ async def probe_capabilities(self) -> set[TransportCapability]:
233
+ """Probe what this serial connection can do."""
234
+ caps = set()
235
+
236
+ if not self.is_connected:
237
+ return caps
238
+
239
+ if self._is_repl:
240
+ # MicroPython REPL - check what modules exist
241
+ result = await self.send_command("dir()")
242
+ if result.success and result.response:
243
+ response = result.response.lower()
244
+
245
+ # Check for arm control
246
+ if "arm" in response or "servo" in response:
247
+ caps.add(TransportCapability.ARM_POSITION)
248
+ caps.add(TransportCapability.GRIPPER)
249
+ caps.add(TransportCapability.RAW_SERVO)
250
+
251
+ # Check for locomotion
252
+ if "gait" in response or "motion" in response:
253
+ caps.add(TransportCapability.WALK)
254
+ caps.add(TransportCapability.POSTURE)
255
+
256
+ # Check for sensors
257
+ if "ultrasonic" in response or "sensor" in response:
258
+ caps.add(TransportCapability.ULTRASONIC)
259
+
260
+ if "imu" in response:
261
+ caps.add(TransportCapability.IMU)
262
+
263
+ caps.add(TransportCapability.BIDIRECTIONAL)
264
+ return caps
265
+
266
+ async def _detect_repl(self) -> bool:
267
+ """Detect if this is a MicroPython REPL."""
268
+ if not self._serial:
269
+ return False
270
+
271
+ try:
272
+ # Send Ctrl+C to interrupt any running code
273
+ self._serial.write(b"\x03")
274
+ await asyncio.sleep(0.3)
275
+
276
+ # Send empty line to trigger prompt
277
+ self._serial.write(b"\r\n")
278
+ await asyncio.sleep(0.3)
279
+
280
+ # Read response
281
+ response = b""
282
+ while self._serial.in_waiting > 0:
283
+ response += self._serial.read(self._serial.in_waiting)
284
+ await asyncio.sleep(0.05)
285
+
286
+ text = response.decode('utf-8', errors='replace')
287
+ return self.config.repl_prompt in text or "MicroPython" in text
288
+
289
+ except Exception:
290
+ return False
291
+
292
+ async def _read_loop(self) -> None:
293
+ """Background loop to read incoming data."""
294
+ while self.is_connected and self._serial:
295
+ try:
296
+ if self._serial.in_waiting > 0:
297
+ data = self._serial.read(self._serial.in_waiting)
298
+ self._response_buffer += data
299
+ await self._notify_handlers(data)
300
+ await asyncio.sleep(0.1)
301
+ except asyncio.CancelledError:
302
+ break
303
+ except Exception:
304
+ await asyncio.sleep(0.1)
305
+
306
+ # ========== MicroPython REPL Commands ==========
307
+ # These are specific to MechDog's MicroPython firmware
308
+
309
+ async def arm_move(self, positions: list[tuple[int, int]]) -> CommandResult:
310
+ """
311
+ Move arm servos to specified positions.
312
+
313
+ Args:
314
+ positions: List of (servo_id, angle) tuples
315
+ MechDog arm: servos 1-5 typically
316
+ Gripper: servo 10-12 depending on mount
317
+
318
+ Example:
319
+ await transport.arm_move([(1, 90), (2, 45), (3, 60)])
320
+ """
321
+ pos_str = ", ".join(f"({sid}, {angle})" for sid, angle in positions)
322
+ cmd = f"arm.arm_move([{pos_str}])"
323
+ return await self.send_command(cmd)
324
+
325
+ async def gripper_open(self) -> CommandResult:
326
+ """Open the gripper (assumes servo ID 10)."""
327
+ return await self.send_command("arm.gripper_open()")
328
+
329
+ async def gripper_close(self) -> CommandResult:
330
+ """Close the gripper."""
331
+ return await self.send_command("arm.gripper_close()")
332
+
333
+ async def get_servo_position(self, servo_id: int) -> Optional[int]:
334
+ """Read current position of a servo."""
335
+ result = await self.send_command(f"arm.read_servo({servo_id})")
336
+ if result.success and result.response:
337
+ # Parse response like "90" or ">>> 90"
338
+ match = re.search(r"(\d+)", result.response)
339
+ if match:
340
+ return int(match.group(1))
341
+ return None
342
+
343
+ async def execute_python(self, code: str) -> CommandResult:
344
+ """Execute arbitrary Python code on the robot."""
345
+ return await self.send_command(code)
ate/urdf/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ Markerless URDF Generation Module.
3
+
4
+ This module provides tools for generating URDF robot descriptions from
5
+ webcam video without physical markers, using foundation computer vision
6
+ models (SAM 2, Depth Anything V2) for perception.
7
+
8
+ Main commands:
9
+ - ate urdf scan capture - Capture video and annotate links
10
+ - ate urdf scan segment - Generate segmented point clouds
11
+ - ate urdf scan optimize - Fit kinematic parameters
12
+ - ate urdf scan mesh - Generate visual/collision meshes
13
+ - ate urdf scan synthesize - Generate final URDF
14
+ - ate urdf scan - Run full pipeline
15
+
16
+ Usage:
17
+ ate urdf scan --output ./my_robot/ --name my_robot --scale-ref "gripper:85mm"
18
+ """
19
+
20
+ from .scan_session import ScanSession, ScanSessionError
21
+ from .scale import parse_scale_ref, parse_measurement
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "ScanSession",
27
+ "ScanSessionError",
28
+ "parse_scale_ref",
29
+ "parse_measurement",
30
+ ]