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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +12 -0
- ate/behaviors/approach.py +399 -0
- ate/cli.py +855 -4551
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +402 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +18 -6
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +360 -24
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +16 -0
- ate/interfaces/base.py +2 -0
- ate/interfaces/sensors.py +247 -0
- ate/llm_proxy.py +239 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +42 -3
- ate/recording/session.py +12 -2
- ate/recording/visual.py +416 -0
- ate/robot/__init__.py +142 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +88 -3
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +143 -11
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +104 -2
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +6 -0
- ate/robot/registry.py +5 -2
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +285 -3
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +9 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.1.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/top_level.txt +0 -0
ate/transports/hybrid.py
ADDED
|
@@ -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
|