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/commands/bridge.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge commands for FoodforThought CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- ate bridge connect - Connect to robot interactively
|
|
6
|
+
- ate bridge send - Send single command
|
|
7
|
+
- ate bridge record - Record session for primitive creation
|
|
8
|
+
- ate bridge replay - Replay a recorded session
|
|
9
|
+
- ate bridge serve - Start WebSocket server for Artifex integration
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bridge_connect(client, port: str, transport: str,
|
|
20
|
+
baud_rate: int, protocol_id: Optional[str]) -> None:
|
|
21
|
+
"""Connect to a robot via serial or BLE and start interactive session."""
|
|
22
|
+
print(f"\n{'=' * 60}")
|
|
23
|
+
print("FoodForThought Bridge - Robot Communication Interface")
|
|
24
|
+
print(f"{'=' * 60}\n")
|
|
25
|
+
|
|
26
|
+
if transport == "serial":
|
|
27
|
+
try:
|
|
28
|
+
import serial
|
|
29
|
+
except ImportError:
|
|
30
|
+
print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
print(f"Connecting to {port} at {baud_rate} baud...")
|
|
34
|
+
try:
|
|
35
|
+
ser = serial.Serial(port, baud_rate, timeout=1)
|
|
36
|
+
print(f"✓ Connected to {port}")
|
|
37
|
+
print(f"\nInteractive mode. Type commands to send to robot.")
|
|
38
|
+
print("Special commands:")
|
|
39
|
+
print(" .quit - Exit bridge")
|
|
40
|
+
print(" .record - Start recording for primitive creation")
|
|
41
|
+
print(" .stop - Stop recording")
|
|
42
|
+
print(" .save - Save recorded session")
|
|
43
|
+
print(" .protocol - Show loaded protocol info")
|
|
44
|
+
print("-" * 60 + "\n")
|
|
45
|
+
|
|
46
|
+
recording = False
|
|
47
|
+
recorded_commands = []
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
cmd = input("bridge> ").strip()
|
|
52
|
+
if not cmd:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if cmd == ".quit":
|
|
56
|
+
break
|
|
57
|
+
elif cmd == ".record":
|
|
58
|
+
recording = True
|
|
59
|
+
recorded_commands = []
|
|
60
|
+
print("Recording started...")
|
|
61
|
+
continue
|
|
62
|
+
elif cmd == ".stop":
|
|
63
|
+
recording = False
|
|
64
|
+
print(f"Recording stopped. {len(recorded_commands)} commands recorded.")
|
|
65
|
+
continue
|
|
66
|
+
elif cmd == ".save":
|
|
67
|
+
if recorded_commands:
|
|
68
|
+
filename = f"session_{int(time.time())}.json"
|
|
69
|
+
with open(filename, "w") as f:
|
|
70
|
+
json.dump({
|
|
71
|
+
"port": port,
|
|
72
|
+
"baud_rate": baud_rate,
|
|
73
|
+
"commands": recorded_commands
|
|
74
|
+
}, f, indent=2)
|
|
75
|
+
print(f"Session saved to {filename}")
|
|
76
|
+
else:
|
|
77
|
+
print("No commands recorded yet.")
|
|
78
|
+
continue
|
|
79
|
+
elif cmd == ".protocol":
|
|
80
|
+
if protocol_id:
|
|
81
|
+
from . import protocol
|
|
82
|
+
protocol.protocol_get(client, protocol_id)
|
|
83
|
+
else:
|
|
84
|
+
print("No protocol loaded. Use --protocol flag to specify.")
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Send command to robot
|
|
88
|
+
ser.write((cmd + "\n").encode())
|
|
89
|
+
if recording:
|
|
90
|
+
recorded_commands.append({
|
|
91
|
+
"command": cmd,
|
|
92
|
+
"timestamp": time.time()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# Read response
|
|
96
|
+
time.sleep(0.1)
|
|
97
|
+
response = ser.read_all().decode("utf-8", errors="replace").strip()
|
|
98
|
+
if response:
|
|
99
|
+
print(f"< {response}")
|
|
100
|
+
if recording:
|
|
101
|
+
recorded_commands[-1]["response"] = response
|
|
102
|
+
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
ser.close()
|
|
107
|
+
print("\n✓ Disconnected")
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"Error connecting to {port}: {e}", file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
elif transport == "ble":
|
|
114
|
+
print("BLE bridge requires async operation. Starting BLE session...")
|
|
115
|
+
_bridge_ble_connect(port)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _bridge_ble_connect(address: str) -> None:
|
|
119
|
+
"""Connect to robot via BLE (requires bleak)."""
|
|
120
|
+
try:
|
|
121
|
+
import asyncio
|
|
122
|
+
from bleak import BleakClient
|
|
123
|
+
except ImportError:
|
|
124
|
+
print("Error: bleak is required for BLE. Install with: pip install bleak", file=sys.stderr)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
async def ble_session():
|
|
128
|
+
print(f"Connecting to BLE device {address}...")
|
|
129
|
+
try:
|
|
130
|
+
async with BleakClient(address) as client:
|
|
131
|
+
print(f"✓ Connected to {address}")
|
|
132
|
+
|
|
133
|
+
print("\nAvailable services:")
|
|
134
|
+
for service in client.services:
|
|
135
|
+
print(f" {service.uuid}: {service.description or 'Unknown'}")
|
|
136
|
+
for char in service.characteristics:
|
|
137
|
+
props = ", ".join(char.properties)
|
|
138
|
+
print(f" └─ {char.uuid} [{props}]")
|
|
139
|
+
|
|
140
|
+
print("\nInteractive mode. Commands:")
|
|
141
|
+
print(" read <uuid> - Read characteristic")
|
|
142
|
+
print(" write <uuid> <hex> - Write to characteristic")
|
|
143
|
+
print(" notify <uuid> - Subscribe to notifications")
|
|
144
|
+
print(" .quit - Exit")
|
|
145
|
+
print("-" * 60 + "\n")
|
|
146
|
+
|
|
147
|
+
while True:
|
|
148
|
+
try:
|
|
149
|
+
cmd = input("ble> ").strip()
|
|
150
|
+
if not cmd:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
if cmd == ".quit":
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
parts = cmd.split()
|
|
157
|
+
if parts[0] == "read" and len(parts) >= 2:
|
|
158
|
+
uuid = parts[1]
|
|
159
|
+
try:
|
|
160
|
+
data = await client.read_gatt_char(uuid)
|
|
161
|
+
print(f"< {data.hex()} ({data})")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"Error reading: {e}")
|
|
164
|
+
|
|
165
|
+
elif parts[0] == "write" and len(parts) >= 3:
|
|
166
|
+
uuid = parts[1]
|
|
167
|
+
hex_data = parts[2]
|
|
168
|
+
try:
|
|
169
|
+
data = bytes.fromhex(hex_data)
|
|
170
|
+
await client.write_gatt_char(uuid, data)
|
|
171
|
+
print(f"✓ Written {hex_data}")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"Error writing: {e}")
|
|
174
|
+
|
|
175
|
+
elif parts[0] == "notify" and len(parts) >= 2:
|
|
176
|
+
uuid = parts[1]
|
|
177
|
+
|
|
178
|
+
def callback(sender, data):
|
|
179
|
+
print(f"[{sender}] {data.hex()}")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
await client.start_notify(uuid, callback)
|
|
183
|
+
print(f"✓ Subscribed to {uuid}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"Error subscribing: {e}")
|
|
186
|
+
|
|
187
|
+
else:
|
|
188
|
+
print("Unknown command. Use read/write/notify or .quit")
|
|
189
|
+
|
|
190
|
+
except KeyboardInterrupt:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
print("\n✓ Disconnected")
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"Error connecting to BLE device: {e}", file=sys.stderr)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
asyncio.run(ble_session())
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def bridge_send(client, port: str, command: str, transport: str,
|
|
203
|
+
baud_rate: int, wait: float) -> None:
|
|
204
|
+
"""Send a single command to robot and print response."""
|
|
205
|
+
if transport == "serial":
|
|
206
|
+
try:
|
|
207
|
+
import serial
|
|
208
|
+
except ImportError:
|
|
209
|
+
print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
ser = serial.Serial(port, baud_rate, timeout=1)
|
|
214
|
+
ser.write((command + "\n").encode())
|
|
215
|
+
time.sleep(wait)
|
|
216
|
+
response = ser.read_all().decode("utf-8", errors="replace").strip()
|
|
217
|
+
if response:
|
|
218
|
+
print(response)
|
|
219
|
+
ser.close()
|
|
220
|
+
except Exception as e:
|
|
221
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
else:
|
|
224
|
+
print("BLE send not yet implemented. Use bridge connect for BLE.", file=sys.stderr)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def bridge_record(client, port: str, output: str, transport: str,
|
|
228
|
+
baud_rate: int, primitive_name: Optional[str]) -> None:
|
|
229
|
+
"""Record a command session for creating a primitive skill."""
|
|
230
|
+
print(f"\n{'=' * 60}")
|
|
231
|
+
print("FoodForThought Bridge - Recording Mode")
|
|
232
|
+
print(f"{'=' * 60}\n")
|
|
233
|
+
|
|
234
|
+
if transport != "serial":
|
|
235
|
+
print("Recording currently only supports serial connections", file=sys.stderr)
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
import serial
|
|
240
|
+
except ImportError:
|
|
241
|
+
print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
|
|
244
|
+
print(f"Connecting to {port} at {baud_rate} baud...")
|
|
245
|
+
try:
|
|
246
|
+
ser = serial.Serial(port, baud_rate, timeout=1)
|
|
247
|
+
print(f"✓ Connected and recording to {output}")
|
|
248
|
+
print(f"\nType commands to send. Ctrl+C to stop and save.\n")
|
|
249
|
+
|
|
250
|
+
recorded = {
|
|
251
|
+
"name": primitive_name or f"recorded_skill_{int(time.time())}",
|
|
252
|
+
"port": port,
|
|
253
|
+
"baud_rate": baud_rate,
|
|
254
|
+
"start_time": time.time(),
|
|
255
|
+
"commands": []
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
while True:
|
|
259
|
+
try:
|
|
260
|
+
cmd = input(f"[REC] bridge> ").strip()
|
|
261
|
+
if not cmd:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
timestamp = time.time()
|
|
265
|
+
ser.write((cmd + "\n").encode())
|
|
266
|
+
time.sleep(0.1)
|
|
267
|
+
response = ser.read_all().decode("utf-8", errors="replace").strip()
|
|
268
|
+
|
|
269
|
+
entry = {
|
|
270
|
+
"command": cmd,
|
|
271
|
+
"timestamp": timestamp - recorded["start_time"],
|
|
272
|
+
"response": response
|
|
273
|
+
}
|
|
274
|
+
recorded["commands"].append(entry)
|
|
275
|
+
|
|
276
|
+
if response:
|
|
277
|
+
print(f"< {response}")
|
|
278
|
+
|
|
279
|
+
except KeyboardInterrupt:
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
ser.close()
|
|
283
|
+
recorded["end_time"] = time.time()
|
|
284
|
+
recorded["duration"] = recorded["end_time"] - recorded["start_time"]
|
|
285
|
+
|
|
286
|
+
with open(output, "w") as f:
|
|
287
|
+
json.dump(recorded, f, indent=2)
|
|
288
|
+
|
|
289
|
+
print(f"\n✓ Recorded {len(recorded['commands'])} commands")
|
|
290
|
+
print(f"✓ Saved to {output}")
|
|
291
|
+
print(f"\nTo create a primitive from this recording:")
|
|
292
|
+
print(f" ate primitive init --from-recording {output}")
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def bridge_replay(client, recording_file: str, port: str, transport: str,
|
|
300
|
+
baud_rate: int, speed: float) -> None:
|
|
301
|
+
"""Replay a recorded session."""
|
|
302
|
+
if not Path(recording_file).exists():
|
|
303
|
+
print(f"Error: Recording file not found: {recording_file}", file=sys.stderr)
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
with open(recording_file) as f:
|
|
307
|
+
recording = json.load(f)
|
|
308
|
+
|
|
309
|
+
if transport != "serial":
|
|
310
|
+
print("Replay currently only supports serial connections", file=sys.stderr)
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
import serial
|
|
315
|
+
except ImportError:
|
|
316
|
+
print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
|
|
319
|
+
print(f"\n{'=' * 60}")
|
|
320
|
+
print(f"Replaying: {recording.get('name', recording_file)}")
|
|
321
|
+
print(f"Commands: {len(recording.get('commands', []))}")
|
|
322
|
+
print(f"Speed: {speed}x")
|
|
323
|
+
print(f"{'=' * 60}\n")
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
ser = serial.Serial(port, baud_rate, timeout=1)
|
|
327
|
+
commands = recording.get("commands", [])
|
|
328
|
+
|
|
329
|
+
prev_timestamp = 0
|
|
330
|
+
for i, entry in enumerate(commands):
|
|
331
|
+
timestamp = entry.get("timestamp", 0)
|
|
332
|
+
delay = (timestamp - prev_timestamp) / speed
|
|
333
|
+
if delay > 0 and i > 0:
|
|
334
|
+
time.sleep(delay)
|
|
335
|
+
prev_timestamp = timestamp
|
|
336
|
+
|
|
337
|
+
cmd = entry.get("command", "")
|
|
338
|
+
print(f"[{i + 1}/{len(commands)}] > {cmd}")
|
|
339
|
+
ser.write((cmd + "\n").encode())
|
|
340
|
+
|
|
341
|
+
time.sleep(0.1)
|
|
342
|
+
response = ser.read_all().decode("utf-8", errors="replace").strip()
|
|
343
|
+
if response:
|
|
344
|
+
print(f" < {response}")
|
|
345
|
+
|
|
346
|
+
ser.close()
|
|
347
|
+
print(f"\n✓ Replay complete")
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f"Error during replay: {e}", file=sys.stderr)
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def bridge_serve(port: int, verbose: bool) -> None:
|
|
355
|
+
"""Start WebSocket server for Artifex Desktop integration."""
|
|
356
|
+
from ate.bridge_server import run_bridge_server
|
|
357
|
+
run_bridge_server(port=port, verbose=verbose)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def register_parser(subparsers):
|
|
361
|
+
"""Register bridge commands with argparse."""
|
|
362
|
+
import argparse
|
|
363
|
+
|
|
364
|
+
bridge_parser = subparsers.add_parser("bridge", help="Interactive robot communication bridge")
|
|
365
|
+
bridge_subparsers = bridge_parser.add_subparsers(dest="bridge_action", help="Bridge action")
|
|
366
|
+
|
|
367
|
+
# bridge connect
|
|
368
|
+
bridge_connect_parser = bridge_subparsers.add_parser("connect",
|
|
369
|
+
help="Connect to robot interactively")
|
|
370
|
+
bridge_connect_parser.add_argument("port", help="Serial port or BLE address")
|
|
371
|
+
bridge_connect_parser.add_argument("-t", "--transport", default="serial",
|
|
372
|
+
choices=["serial", "ble"],
|
|
373
|
+
help="Transport type (default: serial)")
|
|
374
|
+
bridge_connect_parser.add_argument("-b", "--baud", type=int, default=115200,
|
|
375
|
+
help="Baud rate for serial (default: 115200)")
|
|
376
|
+
bridge_connect_parser.add_argument("-p", "--protocol", help="Protocol ID for command hints")
|
|
377
|
+
|
|
378
|
+
# bridge send
|
|
379
|
+
bridge_send_parser = bridge_subparsers.add_parser("send", help="Send single command")
|
|
380
|
+
bridge_send_parser.add_argument("port", help="Serial port or BLE address")
|
|
381
|
+
bridge_send_parser.add_argument("command", help="Command to send")
|
|
382
|
+
bridge_send_parser.add_argument("-t", "--transport", default="serial",
|
|
383
|
+
choices=["serial", "ble"],
|
|
384
|
+
help="Transport type (default: serial)")
|
|
385
|
+
bridge_send_parser.add_argument("-b", "--baud", type=int, default=115200,
|
|
386
|
+
help="Baud rate for serial (default: 115200)")
|
|
387
|
+
bridge_send_parser.add_argument("-w", "--wait", type=float, default=0.5,
|
|
388
|
+
help="Wait time for response in seconds (default: 0.5)")
|
|
389
|
+
|
|
390
|
+
# bridge record
|
|
391
|
+
bridge_record_parser = bridge_subparsers.add_parser("record",
|
|
392
|
+
help="Record session for primitive creation")
|
|
393
|
+
bridge_record_parser.add_argument("port", help="Serial port or BLE address")
|
|
394
|
+
bridge_record_parser.add_argument("-o", "--output", default="./recording.json",
|
|
395
|
+
help="Output file (default: ./recording.json)")
|
|
396
|
+
bridge_record_parser.add_argument("-t", "--transport", default="serial",
|
|
397
|
+
choices=["serial", "ble"],
|
|
398
|
+
help="Transport type (default: serial)")
|
|
399
|
+
bridge_record_parser.add_argument("-b", "--baud", type=int, default=115200,
|
|
400
|
+
help="Baud rate for serial (default: 115200)")
|
|
401
|
+
bridge_record_parser.add_argument("-n", "--name", help="Primitive skill name")
|
|
402
|
+
|
|
403
|
+
# bridge replay
|
|
404
|
+
bridge_replay_parser = bridge_subparsers.add_parser("replay", help="Replay a recorded session")
|
|
405
|
+
bridge_replay_parser.add_argument("recording", help="Path to recording file")
|
|
406
|
+
bridge_replay_parser.add_argument("port", help="Serial port or BLE address")
|
|
407
|
+
bridge_replay_parser.add_argument("-t", "--transport", default="serial",
|
|
408
|
+
choices=["serial", "ble"],
|
|
409
|
+
help="Transport type (default: serial)")
|
|
410
|
+
bridge_replay_parser.add_argument("-b", "--baud", type=int, default=115200,
|
|
411
|
+
help="Baud rate for serial (default: 115200)")
|
|
412
|
+
bridge_replay_parser.add_argument("-s", "--speed", type=float, default=1.0,
|
|
413
|
+
help="Playback speed multiplier (default: 1.0)")
|
|
414
|
+
|
|
415
|
+
# bridge serve
|
|
416
|
+
bridge_serve_parser = bridge_subparsers.add_parser("serve",
|
|
417
|
+
help="Start WebSocket server for Artifex Desktop integration",
|
|
418
|
+
description="""Start the ATE Bridge Server for Artifex Desktop integration.
|
|
419
|
+
|
|
420
|
+
This server enables sim-to-real transfer by bridging Artifex Desktop to physical robot hardware.
|
|
421
|
+
|
|
422
|
+
WORKFLOW:
|
|
423
|
+
1. Start this server: ate bridge serve -v
|
|
424
|
+
2. Connect your robot via USB serial
|
|
425
|
+
3. In Artifex Desktop, use the Hardware panel or AI tools to connect
|
|
426
|
+
4. Control your robot directly from the Artifex interface""",
|
|
427
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
428
|
+
bridge_serve_parser.add_argument("-p", "--port", type=int, default=8765,
|
|
429
|
+
help="WebSocket port (default: 8765)")
|
|
430
|
+
bridge_serve_parser.add_argument("-v", "--verbose", action="store_true",
|
|
431
|
+
help="Enable verbose logging")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def handle(client, args):
|
|
435
|
+
"""Handle bridge commands."""
|
|
436
|
+
if args.bridge_action == "connect":
|
|
437
|
+
bridge_connect(client, args.port, args.transport, args.baud, args.protocol)
|
|
438
|
+
elif args.bridge_action == "send":
|
|
439
|
+
bridge_send(client, args.port, args.command, args.transport, args.baud, args.wait)
|
|
440
|
+
elif args.bridge_action == "record":
|
|
441
|
+
bridge_record(client, args.port, args.output, args.transport, args.baud, args.name)
|
|
442
|
+
elif args.bridge_action == "replay":
|
|
443
|
+
bridge_replay(client, args.recording, args.port, args.transport, args.baud, args.speed)
|
|
444
|
+
elif args.bridge_action == "serve":
|
|
445
|
+
bridge_serve(args.port, args.verbose)
|
|
446
|
+
else:
|
|
447
|
+
print("Usage: ate bridge {connect|send|record|replay|serve}")
|
|
448
|
+
sys.exit(1)
|
ate/commands/data.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data management commands for FoodforThought CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- ate data upload - Upload dataset/sensor logs
|
|
6
|
+
- ate data list - List datasets
|
|
7
|
+
- ate data promote - Promote dataset to next stage
|
|
8
|
+
- ate data export - Export dataset in specified format
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def data_upload(client, path: str, skill: str, stage: str) -> None:
|
|
18
|
+
"""Upload dataset/sensor logs."""
|
|
19
|
+
data_path = Path(path)
|
|
20
|
+
|
|
21
|
+
if not data_path.exists():
|
|
22
|
+
print(f"Error: Path not found: {path}", file=sys.stderr)
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
print(f"Uploading data...")
|
|
26
|
+
print(f" Path: {path}")
|
|
27
|
+
print(f" Skill: {skill}")
|
|
28
|
+
print(f" Stage: {stage}")
|
|
29
|
+
|
|
30
|
+
# Count files
|
|
31
|
+
if data_path.is_dir():
|
|
32
|
+
files = list(data_path.rglob("*"))
|
|
33
|
+
file_count = len([f for f in files if f.is_file()])
|
|
34
|
+
total_size = sum(f.stat().st_size for f in files if f.is_file())
|
|
35
|
+
else:
|
|
36
|
+
file_count = 1
|
|
37
|
+
total_size = data_path.stat().st_size
|
|
38
|
+
|
|
39
|
+
print(f" Files: {file_count}")
|
|
40
|
+
print(f" Size: {total_size / 1024 / 1024:.1f} MB")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
response = client._request("POST", "/datasets/upload", json={
|
|
44
|
+
"skillId": skill,
|
|
45
|
+
"stage": stage,
|
|
46
|
+
"fileCount": file_count,
|
|
47
|
+
"totalSize": total_size,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
dataset = response.get("dataset", {})
|
|
51
|
+
print(f"\n✓ Dataset uploaded!")
|
|
52
|
+
print(f" Dataset ID: {dataset.get('id')}")
|
|
53
|
+
print(f" Stage: {dataset.get('stage')}")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"\n✗ Upload failed: {e}", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def data_list(client, skill: Optional[str], stage: Optional[str]) -> None:
|
|
61
|
+
"""List datasets."""
|
|
62
|
+
print("Fetching datasets...")
|
|
63
|
+
|
|
64
|
+
params = {}
|
|
65
|
+
if skill:
|
|
66
|
+
params["skill"] = skill
|
|
67
|
+
if stage:
|
|
68
|
+
params["stage"] = stage
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
response = client._request("GET", "/datasets", params=params)
|
|
72
|
+
datasets = response.get("datasets", [])
|
|
73
|
+
|
|
74
|
+
if not datasets:
|
|
75
|
+
print("\nNo datasets found.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
print(f"\n{'=' * 70}")
|
|
79
|
+
print(f"{'Name':<30} {'Stage':<15} {'Size':<15} {'Created':<15}")
|
|
80
|
+
print(f"{'=' * 70}")
|
|
81
|
+
|
|
82
|
+
for ds in datasets:
|
|
83
|
+
name = ds.get("name", "Unnamed")[:28]
|
|
84
|
+
ds_stage = ds.get("stage", "unknown")[:13]
|
|
85
|
+
size = f"{ds.get('size', 0) / 1024 / 1024:.1f} MB"
|
|
86
|
+
created = ds.get("createdAt", "")[:10]
|
|
87
|
+
print(f"{name:<30} {ds_stage:<15} {size:<15} {created:<15}")
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"\n✗ Failed to list: {e}", file=sys.stderr)
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def data_promote(client, dataset_id: str, to_stage: str) -> None:
|
|
95
|
+
"""Promote dataset to next stage."""
|
|
96
|
+
print(f"Promoting dataset...")
|
|
97
|
+
print(f" Dataset: {dataset_id}")
|
|
98
|
+
print(f" New Stage: {to_stage}")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
client._request("PATCH", f"/datasets/{dataset_id}/promote", json={
|
|
102
|
+
"stage": to_stage,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
print(f"\n✓ Dataset promoted to {to_stage}!")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"\n✗ Promotion failed: {e}", file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def data_export(client, dataset_id: str, format: str, output: str) -> None:
|
|
113
|
+
"""Export dataset in specified format."""
|
|
114
|
+
print(f"Exporting dataset...")
|
|
115
|
+
print(f" Dataset: {dataset_id}")
|
|
116
|
+
print(f" Format: {format}")
|
|
117
|
+
print(f" Output: {output}")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
response = client._request("GET", f"/datasets/{dataset_id}/export",
|
|
121
|
+
params={"format": format})
|
|
122
|
+
|
|
123
|
+
# Save export
|
|
124
|
+
output_path = Path(output)
|
|
125
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
export_file = output_path / f"{dataset_id}.{format}"
|
|
128
|
+
with open(export_file, 'w') as f:
|
|
129
|
+
json.dump(response, f, indent=2)
|
|
130
|
+
|
|
131
|
+
print(f"\n✓ Exported to: {export_file}")
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"\n✗ Export failed: {e}", file=sys.stderr)
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def register_parser(subparsers):
|
|
139
|
+
"""Register data commands with argparse."""
|
|
140
|
+
data_parser = subparsers.add_parser("data", help="Dataset and telemetry management")
|
|
141
|
+
data_subparsers = data_parser.add_subparsers(dest="data_action", help="Data action")
|
|
142
|
+
|
|
143
|
+
# data upload
|
|
144
|
+
data_upload_parser = data_subparsers.add_parser("upload", help="Upload sensor data")
|
|
145
|
+
data_upload_parser.add_argument("path", help="Path to data directory or file")
|
|
146
|
+
data_upload_parser.add_argument("-s", "--skill", required=True, help="Associated skill ID")
|
|
147
|
+
data_upload_parser.add_argument("--stage", default="raw",
|
|
148
|
+
choices=["raw", "annotated", "skill-abstracted", "production"],
|
|
149
|
+
help="Data stage (default: raw)")
|
|
150
|
+
|
|
151
|
+
# data list
|
|
152
|
+
data_list_parser = data_subparsers.add_parser("list", help="List datasets")
|
|
153
|
+
data_list_parser.add_argument("-s", "--skill", help="Filter by skill ID")
|
|
154
|
+
data_list_parser.add_argument("--stage", help="Filter by stage")
|
|
155
|
+
|
|
156
|
+
# data promote
|
|
157
|
+
data_promote_parser = data_subparsers.add_parser("promote", help="Promote dataset stage")
|
|
158
|
+
data_promote_parser.add_argument("dataset_id", help="Dataset ID")
|
|
159
|
+
data_promote_parser.add_argument("--to", required=True, dest="to_stage",
|
|
160
|
+
choices=["annotated", "skill-abstracted", "production"],
|
|
161
|
+
help="Target stage")
|
|
162
|
+
|
|
163
|
+
# data export
|
|
164
|
+
data_export_parser = data_subparsers.add_parser("export", help="Export dataset")
|
|
165
|
+
data_export_parser.add_argument("dataset_id", help="Dataset ID")
|
|
166
|
+
data_export_parser.add_argument("-f", "--format", default="rlds",
|
|
167
|
+
choices=["json", "rlds", "lerobot", "hdf5"],
|
|
168
|
+
help="Export format (default: rlds)")
|
|
169
|
+
data_export_parser.add_argument("-o", "--output", default="./export",
|
|
170
|
+
help="Output directory")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def handle(client, args):
|
|
174
|
+
"""Handle data commands."""
|
|
175
|
+
if args.data_action == "upload":
|
|
176
|
+
data_upload(client, args.path, args.skill, args.stage)
|
|
177
|
+
elif args.data_action == "list":
|
|
178
|
+
data_list(client, args.skill, args.stage)
|
|
179
|
+
elif args.data_action == "promote":
|
|
180
|
+
data_promote(client, args.dataset_id, args.to_stage)
|
|
181
|
+
elif args.data_action == "export":
|
|
182
|
+
data_export(client, args.dataset_id, args.format, args.output)
|
|
183
|
+
else:
|
|
184
|
+
print("Usage: ate data {upload|list|promote|export}")
|
|
185
|
+
sys.exit(1)
|