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.
- 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 +100 -0
- ate/behaviors/approach.py +399 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +855 -3995
- 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 +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +39 -0
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +942 -0
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +187 -0
- ate/interfaces/base.py +273 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/sensors.py +247 -0
- ate/interfaces/types.py +371 -0
- ate/llm_proxy.py +239 -0
- ate/mcp_server.py +387 -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 +83 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +415 -0
- ate/recording/upload.py +304 -0
- ate/recording/visual.py +416 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +221 -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 +668 -0
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +3735 -0
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +441 -0
- ate/robot/introspection.py +330 -0
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/manager.py +270 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +281 -0
- ate/robot/registry.py +322 -0
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +675 -0
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +1048 -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.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/transports/ble.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BLE Transport Implementation.
|
|
3
|
+
|
|
4
|
+
Uses bleak library for cross-platform Bluetooth Low Energy communication.
|
|
5
|
+
Tested with HiWonder MechDog but designed to be adaptable to other robots.
|
|
6
|
+
|
|
7
|
+
Key learnings from MechDog reverse engineering:
|
|
8
|
+
- Write commands to FFE1 characteristic
|
|
9
|
+
- Subscribe to FFE2 for responses (CRITICAL - many examples miss this!)
|
|
10
|
+
- Commands are ASCII strings in CMD|...|$ format
|
|
11
|
+
- Some commands work (locomotion), some don't (arm) - firmware dependent
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from typing import Optional
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from bleak import BleakClient, BleakScanner
|
|
20
|
+
from bleak.backends.device import BLEDevice
|
|
21
|
+
BLEAK_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
BLEAK_AVAILABLE = False
|
|
24
|
+
BleakClient = None
|
|
25
|
+
BleakScanner = None
|
|
26
|
+
BLEDevice = None
|
|
27
|
+
|
|
28
|
+
from .base import (
|
|
29
|
+
RobotTransport,
|
|
30
|
+
TransportState,
|
|
31
|
+
TransportCapability,
|
|
32
|
+
ConnectionInfo,
|
|
33
|
+
CommandResult,
|
|
34
|
+
HiWonderProtocol,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Standard HiWonder GATT UUIDs
|
|
39
|
+
HIWONDER_SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb"
|
|
40
|
+
HIWONDER_WRITE_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb" # Write commands here
|
|
41
|
+
HIWONDER_NOTIFY_UUID = "0000ffe2-0000-1000-8000-00805f9b34fb" # Subscribe for responses
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class BLEConfig:
|
|
46
|
+
"""Configuration for BLE transport."""
|
|
47
|
+
write_uuid: str = HIWONDER_WRITE_UUID
|
|
48
|
+
notify_uuid: str = HIWONDER_NOTIFY_UUID
|
|
49
|
+
response_timeout: float = 2.0
|
|
50
|
+
retry_count: int = 3
|
|
51
|
+
probe_on_connect: bool = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BLETransport(RobotTransport):
|
|
55
|
+
"""
|
|
56
|
+
Bluetooth Low Energy transport for robot communication.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
async with BLETransport() as transport:
|
|
60
|
+
if await transport.connect("FAF198B6-D9A4-CC27-7BBA-3159F63A61A9"):
|
|
61
|
+
result = await transport.send_command("CMD|6|$") # Battery query
|
|
62
|
+
print(f"Battery: {result.response}")
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, config: Optional[BLEConfig] = None):
|
|
66
|
+
super().__init__()
|
|
67
|
+
|
|
68
|
+
if not BLEAK_AVAILABLE:
|
|
69
|
+
raise ImportError(
|
|
70
|
+
"bleak is required for BLE transport. "
|
|
71
|
+
"Install with: pip install bleak"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.config = config or BLEConfig()
|
|
75
|
+
self._client: Optional[BleakClient] = None
|
|
76
|
+
self._protocol = HiWonderProtocol()
|
|
77
|
+
self._pending_responses: asyncio.Queue = asyncio.Queue()
|
|
78
|
+
self._response_buffer: list[bytes] = []
|
|
79
|
+
|
|
80
|
+
async def connect(self, address: str, **kwargs) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Connect to a BLE robot.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
address: MAC address or UUID of the device
|
|
86
|
+
**kwargs: Additional options (name hint, timeout)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if connected successfully
|
|
90
|
+
"""
|
|
91
|
+
self._state = TransportState.CONNECTING
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Scan to find the device
|
|
95
|
+
device = await self._find_device(address, kwargs.get("name"))
|
|
96
|
+
if not device:
|
|
97
|
+
self._state = TransportState.ERROR
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Connect
|
|
101
|
+
self._client = BleakClient(device)
|
|
102
|
+
connected = await self._client.connect()
|
|
103
|
+
|
|
104
|
+
if not connected or not self._client.is_connected:
|
|
105
|
+
self._state = TransportState.ERROR
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
# Start notification handler
|
|
109
|
+
await self._client.start_notify(
|
|
110
|
+
self.config.notify_uuid,
|
|
111
|
+
self._notification_handler
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self._state = TransportState.CONNECTED
|
|
115
|
+
self._connection_info = ConnectionInfo(
|
|
116
|
+
transport_type="ble",
|
|
117
|
+
address=address,
|
|
118
|
+
name=device.name,
|
|
119
|
+
rssi=device.rssi if hasattr(device, 'rssi') else None,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Probe capabilities if configured
|
|
123
|
+
if self.config.probe_on_connect:
|
|
124
|
+
self._capabilities = await self.probe_capabilities()
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"BLE connect error: {e}")
|
|
130
|
+
self._state = TransportState.ERROR
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
async def disconnect(self) -> None:
|
|
134
|
+
"""Disconnect from the BLE device."""
|
|
135
|
+
if self._client and self._client.is_connected:
|
|
136
|
+
try:
|
|
137
|
+
await self._client.stop_notify(self.config.notify_uuid)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
try:
|
|
141
|
+
await self._client.disconnect()
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
self._client = None
|
|
146
|
+
self._state = TransportState.DISCONNECTED
|
|
147
|
+
self._connection_info = None
|
|
148
|
+
|
|
149
|
+
async def send(self, data: bytes) -> CommandResult:
|
|
150
|
+
"""Send raw bytes to the robot."""
|
|
151
|
+
if not self.is_connected or not self._client:
|
|
152
|
+
return CommandResult(success=False, error="Not connected")
|
|
153
|
+
|
|
154
|
+
import time
|
|
155
|
+
start = time.perf_counter()
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# Clear pending responses
|
|
159
|
+
while not self._pending_responses.empty():
|
|
160
|
+
try:
|
|
161
|
+
self._pending_responses.get_nowait()
|
|
162
|
+
except asyncio.QueueEmpty:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
# Send command
|
|
166
|
+
await self._client.write_gatt_char(
|
|
167
|
+
self.config.write_uuid,
|
|
168
|
+
data
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Wait for response
|
|
172
|
+
try:
|
|
173
|
+
response = await asyncio.wait_for(
|
|
174
|
+
self._pending_responses.get(),
|
|
175
|
+
timeout=self.config.response_timeout
|
|
176
|
+
)
|
|
177
|
+
latency = (time.perf_counter() - start) * 1000
|
|
178
|
+
|
|
179
|
+
return CommandResult(
|
|
180
|
+
success=True,
|
|
181
|
+
response=response.decode('ascii', errors='replace'),
|
|
182
|
+
raw_data=response,
|
|
183
|
+
latency_ms=latency
|
|
184
|
+
)
|
|
185
|
+
except asyncio.TimeoutError:
|
|
186
|
+
# Command sent but no response (may still have worked)
|
|
187
|
+
latency = (time.perf_counter() - start) * 1000
|
|
188
|
+
return CommandResult(
|
|
189
|
+
success=True,
|
|
190
|
+
error="No response (command may have worked)",
|
|
191
|
+
latency_ms=latency
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return CommandResult(success=False, error=str(e))
|
|
196
|
+
|
|
197
|
+
async def send_command(self, command: str) -> CommandResult:
|
|
198
|
+
"""Send a command string (handles encoding)."""
|
|
199
|
+
if command.startswith("CMD|"):
|
|
200
|
+
# Already formatted
|
|
201
|
+
data = command.encode('ascii')
|
|
202
|
+
else:
|
|
203
|
+
# Try to encode as canonical command
|
|
204
|
+
try:
|
|
205
|
+
data = self._protocol.encode_command(command)
|
|
206
|
+
except ValueError:
|
|
207
|
+
# Unknown command, send as-is
|
|
208
|
+
data = command.encode('ascii')
|
|
209
|
+
|
|
210
|
+
return await self.send(data)
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
async def discover(cls, timeout: float = 5.0) -> list[ConnectionInfo]:
|
|
214
|
+
"""Discover BLE robots."""
|
|
215
|
+
if not BLEAK_AVAILABLE:
|
|
216
|
+
return []
|
|
217
|
+
|
|
218
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
219
|
+
results = []
|
|
220
|
+
|
|
221
|
+
for device in devices:
|
|
222
|
+
# Filter for likely robot devices
|
|
223
|
+
name = device.name or ""
|
|
224
|
+
if any(kw in name.lower() for kw in ["mechdog", "robot", "hiwonder", "unitree"]):
|
|
225
|
+
results.append(ConnectionInfo(
|
|
226
|
+
transport_type="ble",
|
|
227
|
+
address=device.address,
|
|
228
|
+
name=device.name,
|
|
229
|
+
rssi=device.rssi if hasattr(device, 'rssi') else None,
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
return results
|
|
233
|
+
|
|
234
|
+
async def probe_capabilities(self) -> set[TransportCapability]:
|
|
235
|
+
"""
|
|
236
|
+
Probe which capabilities work on this connection.
|
|
237
|
+
|
|
238
|
+
Sends test commands and checks for valid responses.
|
|
239
|
+
This handles the firmware fragmentation problem.
|
|
240
|
+
"""
|
|
241
|
+
caps = set()
|
|
242
|
+
|
|
243
|
+
if not self.is_connected:
|
|
244
|
+
return caps
|
|
245
|
+
|
|
246
|
+
# Always try battery first - good connectivity test
|
|
247
|
+
result = await self.send(b"CMD|6|$")
|
|
248
|
+
if result.success and result.response and "CMD|6|" in result.response:
|
|
249
|
+
caps.add(TransportCapability.BATTERY)
|
|
250
|
+
caps.add(TransportCapability.BIDIRECTIONAL)
|
|
251
|
+
|
|
252
|
+
# Try ultrasonic
|
|
253
|
+
result = await self.send(b"CMD|4|1|$")
|
|
254
|
+
await asyncio.sleep(0.3)
|
|
255
|
+
if result.success and result.response and "CMD|4|" in result.response:
|
|
256
|
+
caps.add(TransportCapability.ULTRASONIC)
|
|
257
|
+
|
|
258
|
+
# We know locomotion works from testing
|
|
259
|
+
# Could verify with a small movement, but risky for capability probe
|
|
260
|
+
caps.add(TransportCapability.WALK)
|
|
261
|
+
caps.add(TransportCapability.POSTURE)
|
|
262
|
+
caps.add(TransportCapability.ACTION_GROUPS)
|
|
263
|
+
|
|
264
|
+
# UPDATE: CMD|7|... commands DO work for arm control!
|
|
265
|
+
# Tested 2026-01-04: gripper, shoulder, elbow all respond to CMD|7
|
|
266
|
+
caps.add(TransportCapability.ARM_POSITION)
|
|
267
|
+
caps.add(TransportCapability.GRIPPER)
|
|
268
|
+
|
|
269
|
+
return caps
|
|
270
|
+
|
|
271
|
+
async def _find_device(
|
|
272
|
+
self,
|
|
273
|
+
address: str,
|
|
274
|
+
name_hint: Optional[str] = None
|
|
275
|
+
) -> Optional["BLEDevice"]:
|
|
276
|
+
"""Find a BLE device by address or name."""
|
|
277
|
+
devices = await BleakScanner.discover(timeout=5.0)
|
|
278
|
+
|
|
279
|
+
for device in devices:
|
|
280
|
+
if device.address == address:
|
|
281
|
+
return device
|
|
282
|
+
if name_hint and device.name and name_hint.lower() in device.name.lower():
|
|
283
|
+
return device
|
|
284
|
+
if device.name and "mechdog" in device.name.lower():
|
|
285
|
+
return device
|
|
286
|
+
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def _notification_handler(self, sender, data: bytes) -> None:
|
|
290
|
+
"""Handle incoming BLE notifications."""
|
|
291
|
+
# Store in buffer
|
|
292
|
+
self._response_buffer.append(data)
|
|
293
|
+
|
|
294
|
+
# Notify async handlers
|
|
295
|
+
asyncio.create_task(self._notify_handlers(data))
|
|
296
|
+
|
|
297
|
+
# Put in queue for synchronous waiting
|
|
298
|
+
try:
|
|
299
|
+
self._pending_responses.put_nowait(data)
|
|
300
|
+
except asyncio.QueueFull:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
# ========== High-Level Commands ==========
|
|
304
|
+
# These use the protocol adapter for clean API
|
|
305
|
+
|
|
306
|
+
async def walk_forward(self, speed: int = 100) -> CommandResult:
|
|
307
|
+
"""Walk forward at given speed (0-200)."""
|
|
308
|
+
return await self.send(f"CMD|3|{speed}|0|0|$".encode())
|
|
309
|
+
|
|
310
|
+
async def walk_backward(self, speed: int = 100) -> CommandResult:
|
|
311
|
+
"""Walk backward at given speed."""
|
|
312
|
+
return await self.send(f"CMD|3|{-speed}|0|0|$".encode())
|
|
313
|
+
|
|
314
|
+
async def stop(self) -> CommandResult:
|
|
315
|
+
"""Stop all movement."""
|
|
316
|
+
return await self.send(b"CMD|3|0|0|0|$")
|
|
317
|
+
|
|
318
|
+
async def stand(self) -> CommandResult:
|
|
319
|
+
"""Stand in normal posture."""
|
|
320
|
+
return await self.send(b"CMD|1|0|$")
|
|
321
|
+
|
|
322
|
+
async def sit(self) -> CommandResult:
|
|
323
|
+
"""Sit down."""
|
|
324
|
+
return await self.send(b"CMD|1|7|$")
|
|
325
|
+
|
|
326
|
+
async def get_battery(self) -> Optional[float]:
|
|
327
|
+
"""Get battery voltage."""
|
|
328
|
+
result = await self.send(b"CMD|6|$")
|
|
329
|
+
if result.response and "CMD|6|" in result.response:
|
|
330
|
+
try:
|
|
331
|
+
parts = result.response.split("|")
|
|
332
|
+
return float(parts[2])
|
|
333
|
+
except (IndexError, ValueError):
|
|
334
|
+
pass
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
async def get_ultrasonic(self) -> Optional[int]:
|
|
338
|
+
"""Get ultrasonic distance in mm."""
|
|
339
|
+
result = await self.send(b"CMD|4|1|$")
|
|
340
|
+
if result.response and "CMD|4|" in result.response:
|
|
341
|
+
try:
|
|
342
|
+
parts = result.response.split("|")
|
|
343
|
+
return int(parts[2])
|
|
344
|
+
except (IndexError, ValueError):
|
|
345
|
+
pass
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
async def run_action_group(self, group_id: int) -> CommandResult:
|
|
349
|
+
"""Run a pre-programmed action group."""
|
|
350
|
+
return await self.send(f"CMD|2|1|{group_id}|$".encode())
|
|
351
|
+
|
|
352
|
+
# ========== ARM Control (CMD|7|...) ==========
|
|
353
|
+
# Tested and confirmed working 2026-01-04
|
|
354
|
+
|
|
355
|
+
async def gripper_open(self) -> CommandResult:
|
|
356
|
+
"""Open the gripper."""
|
|
357
|
+
return await self.send(b"CMD|7|6|$")
|
|
358
|
+
|
|
359
|
+
async def gripper_close(self) -> CommandResult:
|
|
360
|
+
"""Close the gripper."""
|
|
361
|
+
return await self.send(b"CMD|7|7|$")
|
|
362
|
+
|
|
363
|
+
async def shoulder_up(self, repeats: int = 1) -> CommandResult:
|
|
364
|
+
"""Move shoulder joint up (incremental)."""
|
|
365
|
+
result = None
|
|
366
|
+
for _ in range(repeats):
|
|
367
|
+
result = await self.send(b"CMD|7|3|1|$")
|
|
368
|
+
await asyncio.sleep(0.2)
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
async def shoulder_down(self, repeats: int = 1) -> CommandResult:
|
|
372
|
+
"""Move shoulder joint down (incremental)."""
|
|
373
|
+
result = None
|
|
374
|
+
for _ in range(repeats):
|
|
375
|
+
result = await self.send(b"CMD|7|3|-1|$")
|
|
376
|
+
await asyncio.sleep(0.2)
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
async def elbow_extend(self, repeats: int = 1) -> CommandResult:
|
|
380
|
+
"""Extend elbow joint (incremental)."""
|
|
381
|
+
result = None
|
|
382
|
+
for _ in range(repeats):
|
|
383
|
+
result = await self.send(b"CMD|7|4|1|$")
|
|
384
|
+
await asyncio.sleep(0.2)
|
|
385
|
+
return result
|
|
386
|
+
|
|
387
|
+
async def elbow_retract(self, repeats: int = 1) -> CommandResult:
|
|
388
|
+
"""Retract elbow joint (incremental)."""
|
|
389
|
+
result = None
|
|
390
|
+
for _ in range(repeats):
|
|
391
|
+
result = await self.send(b"CMD|7|4|-1|$")
|
|
392
|
+
await asyncio.sleep(0.2)
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
async def arm_to_pickup_position(self) -> None:
|
|
396
|
+
"""Move arm to pickup position (lowered, extended)."""
|
|
397
|
+
await self.gripper_open()
|
|
398
|
+
await asyncio.sleep(0.3)
|
|
399
|
+
await self.shoulder_down(repeats=10)
|
|
400
|
+
await self.elbow_extend(repeats=8)
|
|
401
|
+
|
|
402
|
+
async def arm_to_carry_position(self) -> None:
|
|
403
|
+
"""Move arm to carry position (raised, retracted)."""
|
|
404
|
+
await self.shoulder_up(repeats=10)
|
|
405
|
+
await self.elbow_retract(repeats=8)
|