foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.3__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 +1 -1
- ate/bridge_server.py +622 -0
- ate/cli.py +2625 -242
- ate/compatibility.py +580 -0
- ate/generators/__init__.py +19 -0
- ate/generators/docker_generator.py +461 -0
- ate/generators/hardware_config.py +469 -0
- ate/generators/ros2_generator.py +617 -0
- ate/generators/skill_generator.py +783 -0
- ate/marketplace.py +524 -0
- ate/mcp_server.py +1341 -107
- ate/primitives.py +1016 -0
- ate/robot_setup.py +2222 -0
- ate/skill_schema.py +537 -0
- ate/telemetry/__init__.py +33 -0
- ate/telemetry/cli.py +455 -0
- ate/telemetry/collector.py +444 -0
- ate/telemetry/context.py +318 -0
- ate/telemetry/fleet_agent.py +419 -0
- ate/telemetry/formats/__init__.py +18 -0
- ate/telemetry/formats/hdf5_serializer.py +503 -0
- ate/telemetry/formats/mcap_serializer.py +457 -0
- ate/telemetry/types.py +334 -0
- foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
- foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
- foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
- mechdog_labeled/__init__.py +3 -0
- mechdog_labeled/primitives.py +113 -0
- mechdog_labeled/servo_map.py +209 -0
- mechdog_output/__init__.py +3 -0
- mechdog_output/primitives.py +59 -0
- mechdog_output/servo_map.py +203 -0
- test_autodetect/__init__.py +3 -0
- test_autodetect/primitives.py +113 -0
- test_autodetect/servo_map.py +209 -0
- test_full_auto/__init__.py +3 -0
- test_full_auto/primitives.py +113 -0
- test_full_auto/servo_map.py +209 -0
- test_smart_detect/__init__.py +3 -0
- test_smart_detect/primitives.py +113 -0
- test_smart_detect/servo_map.py +209 -0
- foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/entry_points.txt +0 -0
ate/robot_setup.py
ADDED
|
@@ -0,0 +1,2222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Robot Setup Wizard - Automated discovery and primitive skill generation for any robot.
|
|
4
|
+
|
|
5
|
+
This wizard:
|
|
6
|
+
1. Discovers connected USB/serial devices
|
|
7
|
+
2. Enumerates servos/motors on each device
|
|
8
|
+
3. Characterizes each component's parameters
|
|
9
|
+
4. Guides interactive labeling with AI assistance
|
|
10
|
+
5. Generates primitive skills and protocol definitions
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
ate robot-setup # Interactive wizard
|
|
14
|
+
ate robot-setup --port /dev/tty... # Skip device selection
|
|
15
|
+
ate robot-setup --output ./robot # Specify output directory
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import struct
|
|
23
|
+
from dataclasses import dataclass, field, asdict
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import List, Dict, Optional, Any, Tuple, Callable
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
|
|
29
|
+
# Optional imports with fallbacks
|
|
30
|
+
try:
|
|
31
|
+
import serial
|
|
32
|
+
import serial.tools.list_ports
|
|
33
|
+
HAS_SERIAL = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
HAS_SERIAL = False
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import anthropic
|
|
39
|
+
HAS_ANTHROPIC = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
HAS_ANTHROPIC = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DeviceType(str, Enum):
|
|
45
|
+
"""Types of robot communication devices."""
|
|
46
|
+
SERIAL = "serial"
|
|
47
|
+
USB_HID = "usb_hid"
|
|
48
|
+
BLUETOOTH = "bluetooth"
|
|
49
|
+
WIFI = "wifi"
|
|
50
|
+
UNKNOWN = "unknown"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ActuatorType(str, Enum):
|
|
54
|
+
"""Types of actuators."""
|
|
55
|
+
SERIAL_BUS_SERVO = "serial_bus_servo"
|
|
56
|
+
PWM_SERVO = "pwm_servo"
|
|
57
|
+
STEPPER_MOTOR = "stepper_motor"
|
|
58
|
+
DC_MOTOR = "dc_motor"
|
|
59
|
+
LINEAR_ACTUATOR = "linear_actuator"
|
|
60
|
+
UNKNOWN = "unknown"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ProtocolType(str, Enum):
|
|
64
|
+
"""Known servo protocols."""
|
|
65
|
+
HIWONDER = "hiwonder" # HiWonder/LewanSoul serial bus servos
|
|
66
|
+
DYNAMIXEL = "dynamixel" # Dynamixel servos (Robotis)
|
|
67
|
+
FEETECH = "feetech" # Feetech/SCS servos
|
|
68
|
+
WAVESHARE = "waveshare" # Waveshare serial servos
|
|
69
|
+
CUSTOM = "custom"
|
|
70
|
+
UNKNOWN = "unknown"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class DiscoveredDevice:
|
|
75
|
+
"""A discovered communication device."""
|
|
76
|
+
port: str
|
|
77
|
+
device_type: DeviceType
|
|
78
|
+
description: str = ""
|
|
79
|
+
vid: Optional[int] = None
|
|
80
|
+
pid: Optional[int] = None
|
|
81
|
+
serial_number: Optional[str] = None
|
|
82
|
+
manufacturer: Optional[str] = None
|
|
83
|
+
detected_protocol: Optional[ProtocolType] = None
|
|
84
|
+
baud_rate: int = 115200
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ActuatorCharacteristics:
|
|
89
|
+
"""Characteristics of an actuator determined through testing."""
|
|
90
|
+
position_min: int = 0
|
|
91
|
+
position_max: int = 1000
|
|
92
|
+
position_center: int = 500
|
|
93
|
+
speed_min: int = 0
|
|
94
|
+
speed_max: int = 2000
|
|
95
|
+
supports_torque_control: bool = False
|
|
96
|
+
supports_position_read: bool = True
|
|
97
|
+
supports_voltage_read: bool = False
|
|
98
|
+
supports_temperature_read: bool = False
|
|
99
|
+
response_time_ms: float = 0.0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class DiscoveredActuator:
|
|
104
|
+
"""A discovered actuator (servo, motor, etc.)."""
|
|
105
|
+
id: int
|
|
106
|
+
actuator_type: ActuatorType
|
|
107
|
+
protocol: ProtocolType
|
|
108
|
+
characteristics: ActuatorCharacteristics = field(default_factory=ActuatorCharacteristics)
|
|
109
|
+
|
|
110
|
+
# User-assigned labels
|
|
111
|
+
label: str = "" # e.g., "front_left_hip", "arm_shoulder"
|
|
112
|
+
group: str = "" # e.g., "left_front_leg", "arm"
|
|
113
|
+
role: str = "" # e.g., "hip", "knee", "ankle", "shoulder"
|
|
114
|
+
|
|
115
|
+
# Test results
|
|
116
|
+
verified_working: bool = False
|
|
117
|
+
notes: str = ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class RobotProfile:
|
|
122
|
+
"""Complete profile of a discovered robot."""
|
|
123
|
+
name: str = "unnamed_robot"
|
|
124
|
+
robot_type: str = "" # e.g., "quadruped", "humanoid", "arm"
|
|
125
|
+
manufacturer: str = ""
|
|
126
|
+
model: str = ""
|
|
127
|
+
|
|
128
|
+
# Communication
|
|
129
|
+
device: Optional[DiscoveredDevice] = None
|
|
130
|
+
|
|
131
|
+
# Actuators
|
|
132
|
+
actuators: List[DiscoveredActuator] = field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
# Groups (legs, arms, etc.)
|
|
135
|
+
groups: Dict[str, List[int]] = field(default_factory=dict) # group_name -> [actuator_ids]
|
|
136
|
+
|
|
137
|
+
# Metadata
|
|
138
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
139
|
+
wizard_version: str = "1.0.0"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class WizardUI:
|
|
143
|
+
"""Console UI helpers for the wizard."""
|
|
144
|
+
|
|
145
|
+
COLORS = {
|
|
146
|
+
'reset': '\033[0m',
|
|
147
|
+
'bold': '\033[1m',
|
|
148
|
+
'red': '\033[91m',
|
|
149
|
+
'green': '\033[92m',
|
|
150
|
+
'yellow': '\033[93m',
|
|
151
|
+
'blue': '\033[94m',
|
|
152
|
+
'magenta': '\033[95m',
|
|
153
|
+
'cyan': '\033[96m',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def print_header(cls, text: str):
|
|
158
|
+
"""Print a section header."""
|
|
159
|
+
print(f"\n{cls.COLORS['bold']}{cls.COLORS['cyan']}{'='*60}{cls.COLORS['reset']}")
|
|
160
|
+
print(f"{cls.COLORS['bold']}{cls.COLORS['cyan']} {text}{cls.COLORS['reset']}")
|
|
161
|
+
print(f"{cls.COLORS['bold']}{cls.COLORS['cyan']}{'='*60}{cls.COLORS['reset']}\n")
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def print_step(cls, step: int, total: int, text: str):
|
|
165
|
+
"""Print a wizard step indicator."""
|
|
166
|
+
print(f"\n{cls.COLORS['magenta']}[Step {step}/{total}]{cls.COLORS['reset']} {cls.COLORS['bold']}{text}{cls.COLORS['reset']}")
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def print_success(cls, text: str):
|
|
170
|
+
"""Print success message."""
|
|
171
|
+
print(f"{cls.COLORS['green']}✓ {text}{cls.COLORS['reset']}")
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def print_warning(cls, text: str):
|
|
175
|
+
"""Print warning message."""
|
|
176
|
+
print(f"{cls.COLORS['yellow']}⚠ {text}{cls.COLORS['reset']}")
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def print_error(cls, text: str):
|
|
180
|
+
"""Print error message."""
|
|
181
|
+
print(f"{cls.COLORS['red']}✗ {text}{cls.COLORS['reset']}")
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def print_info(cls, text: str):
|
|
185
|
+
"""Print info message."""
|
|
186
|
+
print(f"{cls.COLORS['blue']}ℹ {text}{cls.COLORS['reset']}")
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def prompt(cls, text: str, default: str = "") -> str:
|
|
190
|
+
"""Prompt user for input."""
|
|
191
|
+
if default:
|
|
192
|
+
result = input(f"{text} [{default}]: ").strip()
|
|
193
|
+
return result if result else default
|
|
194
|
+
return input(f"{text}: ").strip()
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def confirm(cls, text: str, default: bool = True) -> bool:
|
|
198
|
+
"""Prompt for yes/no confirmation."""
|
|
199
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
200
|
+
result = input(f"{text} {suffix}: ").strip().lower()
|
|
201
|
+
if not result:
|
|
202
|
+
return default
|
|
203
|
+
return result in ('y', 'yes')
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def select(cls, text: str, options: List[str], allow_multiple: bool = False) -> List[int]:
|
|
207
|
+
"""Prompt user to select from options."""
|
|
208
|
+
print(f"\n{text}")
|
|
209
|
+
for i, opt in enumerate(options, 1):
|
|
210
|
+
print(f" {i}. {opt}")
|
|
211
|
+
|
|
212
|
+
if allow_multiple:
|
|
213
|
+
print(f"\n Enter numbers separated by commas (e.g., 1,3,5) or 'all':")
|
|
214
|
+
|
|
215
|
+
while True:
|
|
216
|
+
selection = input(" Selection: ").strip()
|
|
217
|
+
|
|
218
|
+
if allow_multiple and selection.lower() == 'all':
|
|
219
|
+
return list(range(len(options)))
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
if allow_multiple:
|
|
223
|
+
indices = [int(x.strip()) - 1 for x in selection.split(',')]
|
|
224
|
+
else:
|
|
225
|
+
indices = [int(selection) - 1]
|
|
226
|
+
|
|
227
|
+
if all(0 <= i < len(options) for i in indices):
|
|
228
|
+
return indices
|
|
229
|
+
except ValueError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
print(f" {cls.COLORS['red']}Invalid selection. Please try again.{cls.COLORS['reset']}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ServoProtocolHandler:
|
|
236
|
+
"""Handles communication with serial bus servos."""
|
|
237
|
+
|
|
238
|
+
def __init__(self, port: str, baud_rate: int = 115200, protocol: ProtocolType = ProtocolType.HIWONDER):
|
|
239
|
+
self.port = port
|
|
240
|
+
self.baud_rate = baud_rate
|
|
241
|
+
self.protocol = protocol
|
|
242
|
+
self.serial: Optional[serial.Serial] = None
|
|
243
|
+
|
|
244
|
+
def connect(self) -> bool:
|
|
245
|
+
"""Open serial connection."""
|
|
246
|
+
if not HAS_SERIAL:
|
|
247
|
+
WizardUI.print_error("pyserial not installed. Run: pip install pyserial")
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
self.serial = serial.Serial(self.port, self.baud_rate, timeout=0.1)
|
|
252
|
+
time.sleep(0.1) # Let it stabilize
|
|
253
|
+
return True
|
|
254
|
+
except Exception as e:
|
|
255
|
+
WizardUI.print_error(f"Failed to open {self.port}: {e}")
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def disconnect(self):
|
|
259
|
+
"""Close serial connection."""
|
|
260
|
+
if self.serial:
|
|
261
|
+
self.serial.close()
|
|
262
|
+
self.serial = None
|
|
263
|
+
|
|
264
|
+
def _send_command(self, servo_id: int, cmd: int, params: bytes = b'') -> Optional[bytes]:
|
|
265
|
+
"""Send a command and receive response (HiWonder protocol)."""
|
|
266
|
+
if not self.serial:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
# HiWonder packet: 0x55 0x55 ID LEN CMD [PARAMS...] CHECKSUM
|
|
270
|
+
length = len(params) + 3 # cmd + params + checksum
|
|
271
|
+
packet = bytes([0x55, 0x55, servo_id, length, cmd]) + params
|
|
272
|
+
checksum = (~sum(packet[2:]) & 0xFF)
|
|
273
|
+
packet += bytes([checksum])
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
self.serial.reset_input_buffer()
|
|
277
|
+
self.serial.write(packet)
|
|
278
|
+
self.serial.flush()
|
|
279
|
+
|
|
280
|
+
# Read response header
|
|
281
|
+
time.sleep(0.01)
|
|
282
|
+
header = self.serial.read(5)
|
|
283
|
+
if len(header) < 5:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
if header[0:2] != b'\x55\x55':
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
resp_len = header[3]
|
|
290
|
+
remaining = self.serial.read(resp_len - 2) # -2 because length includes cmd+checksum
|
|
291
|
+
|
|
292
|
+
return header + remaining
|
|
293
|
+
except Exception:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def ping(self, servo_id: int) -> bool:
|
|
297
|
+
"""Check if a servo responds at given ID."""
|
|
298
|
+
if self.protocol == ProtocolType.HIWONDER:
|
|
299
|
+
# Read position command (14)
|
|
300
|
+
response = self._send_command(servo_id, 14)
|
|
301
|
+
return response is not None and len(response) >= 7
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
def read_position(self, servo_id: int) -> Optional[int]:
|
|
305
|
+
"""Read current position of servo."""
|
|
306
|
+
if self.protocol == ProtocolType.HIWONDER:
|
|
307
|
+
response = self._send_command(servo_id, 28) # Read position
|
|
308
|
+
if response and len(response) >= 8:
|
|
309
|
+
pos_low = response[5]
|
|
310
|
+
pos_high = response[6]
|
|
311
|
+
return pos_low | (pos_high << 8)
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
def move_servo(self, servo_id: int, position: int, time_ms: int = 500):
|
|
315
|
+
"""Move servo to position."""
|
|
316
|
+
if self.protocol == ProtocolType.HIWONDER:
|
|
317
|
+
# Move command (1): ID, time_low, time_high, pos_low, pos_high
|
|
318
|
+
params = struct.pack('<HH', time_ms, position)
|
|
319
|
+
self._send_command(servo_id, 1, params)
|
|
320
|
+
|
|
321
|
+
def scan_servos(self, id_range: range = range(0, 32), progress_callback: Optional[Callable[[int, int], None]] = None) -> List[int]:
|
|
322
|
+
"""Scan for servos in ID range."""
|
|
323
|
+
found = []
|
|
324
|
+
total = len(id_range)
|
|
325
|
+
|
|
326
|
+
for i, servo_id in enumerate(id_range):
|
|
327
|
+
if progress_callback:
|
|
328
|
+
progress_callback(i + 1, total)
|
|
329
|
+
|
|
330
|
+
if self.ping(servo_id):
|
|
331
|
+
found.append(servo_id)
|
|
332
|
+
|
|
333
|
+
return found
|
|
334
|
+
|
|
335
|
+
def characterize_servo(self, servo_id: int) -> ActuatorCharacteristics:
|
|
336
|
+
"""Determine servo characteristics through safe testing."""
|
|
337
|
+
chars = ActuatorCharacteristics()
|
|
338
|
+
|
|
339
|
+
# Try to read current position
|
|
340
|
+
pos = self.read_position(servo_id)
|
|
341
|
+
if pos is not None:
|
|
342
|
+
chars.supports_position_read = True
|
|
343
|
+
chars.position_center = pos
|
|
344
|
+
|
|
345
|
+
# HiWonder servos typically have 0-1000 range
|
|
346
|
+
if self.protocol == ProtocolType.HIWONDER:
|
|
347
|
+
chars.position_min = 0
|
|
348
|
+
chars.position_max = 1000
|
|
349
|
+
chars.position_center = 500
|
|
350
|
+
chars.speed_min = 0
|
|
351
|
+
chars.speed_max = 30000 # time in ms (lower = faster)
|
|
352
|
+
|
|
353
|
+
return chars
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class AILabelingAssistant:
|
|
357
|
+
"""AI-powered assistant for labeling robot components."""
|
|
358
|
+
|
|
359
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
360
|
+
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
361
|
+
self.client = None
|
|
362
|
+
|
|
363
|
+
if HAS_ANTHROPIC and self.api_key:
|
|
364
|
+
self.client = anthropic.Anthropic(api_key=self.api_key)
|
|
365
|
+
|
|
366
|
+
def suggest_robot_type(self, num_servos: int, servo_ids: List[int]) -> str:
|
|
367
|
+
"""Suggest robot type based on servo count and ID distribution.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
num_servos: Total number of servos found
|
|
371
|
+
servo_ids: List of servo IDs (used to detect core vs peripheral servos)
|
|
372
|
+
"""
|
|
373
|
+
# Analyze servo ID distribution to find "core" actuators
|
|
374
|
+
# Many robots have main actuators in low IDs (0-15) and peripherals in higher IDs
|
|
375
|
+
core_servo_count = self._count_core_servos(servo_ids)
|
|
376
|
+
|
|
377
|
+
# Use core count for detection if it differs significantly from total
|
|
378
|
+
effective_count = core_servo_count if core_servo_count < num_servos * 0.7 else num_servos
|
|
379
|
+
|
|
380
|
+
if not self.client:
|
|
381
|
+
# Rule-based detection
|
|
382
|
+
return self._detect_robot_type_by_count(effective_count, num_servos)
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
response = self.client.messages.create(
|
|
386
|
+
model="claude-3-haiku-20240307",
|
|
387
|
+
max_tokens=150,
|
|
388
|
+
messages=[{
|
|
389
|
+
"role": "user",
|
|
390
|
+
"content": f"""Detected {num_servos} servos total, but analysis suggests {effective_count} are main actuators.
|
|
391
|
+
Servo IDs found: {sorted(servo_ids)[:20]}{'...' if len(servo_ids) > 20 else ''}
|
|
392
|
+
|
|
393
|
+
What type of robot is this likely to be?
|
|
394
|
+
|
|
395
|
+
Common patterns:
|
|
396
|
+
- 12 servos: quadruped (4 legs × 3 joints)
|
|
397
|
+
- 13-15 servos: quadruped_with_arm (4 legs + arm)
|
|
398
|
+
- 18 servos: hexapod (6 legs × 3 joints)
|
|
399
|
+
- 6 servos: 6dof_arm
|
|
400
|
+
- 16-17 servos: humanoid_basic (arms + legs only)
|
|
401
|
+
- 18-20 servos: humanoid (arms + legs + some torso)
|
|
402
|
+
- 21-24 servos: humanoid_advanced (full body)
|
|
403
|
+
- 25+ servos: humanoid_full (with dexterous hands)
|
|
404
|
+
|
|
405
|
+
Reply with just the robot type from the list above."""
|
|
406
|
+
}]
|
|
407
|
+
)
|
|
408
|
+
return response.content[0].text.strip().lower()
|
|
409
|
+
except Exception:
|
|
410
|
+
return self._detect_robot_type_by_count(effective_count, num_servos)
|
|
411
|
+
|
|
412
|
+
def _count_core_servos(self, servo_ids: List[int]) -> int:
|
|
413
|
+
"""Count core servos by analyzing ID distribution.
|
|
414
|
+
|
|
415
|
+
Heuristics:
|
|
416
|
+
- IDs 0-15 are typically main actuators
|
|
417
|
+
- Look for gaps in ID sequence as boundaries
|
|
418
|
+
- Contiguous low IDs are more likely to be core actuators
|
|
419
|
+
"""
|
|
420
|
+
if not servo_ids:
|
|
421
|
+
return 0
|
|
422
|
+
|
|
423
|
+
sorted_ids = sorted(servo_ids)
|
|
424
|
+
|
|
425
|
+
# Count servos in the "core" range (0-15)
|
|
426
|
+
core_range_count = sum(1 for id in sorted_ids if id <= 15)
|
|
427
|
+
|
|
428
|
+
# Also look for the first significant gap (gap > 1)
|
|
429
|
+
last_id = -1
|
|
430
|
+
contiguous_count = 0
|
|
431
|
+
for id in sorted_ids:
|
|
432
|
+
if last_id >= 0 and id - last_id > 1:
|
|
433
|
+
# Found a gap - servos before this are likely core
|
|
434
|
+
break
|
|
435
|
+
contiguous_count += 1
|
|
436
|
+
last_id = id
|
|
437
|
+
|
|
438
|
+
# Use the more conservative estimate
|
|
439
|
+
return min(core_range_count, contiguous_count) if contiguous_count > 0 else core_range_count
|
|
440
|
+
|
|
441
|
+
def _detect_robot_type_by_count(self, effective_count: int, total_count: int) -> str:
|
|
442
|
+
"""Detect robot type based on servo count."""
|
|
443
|
+
# Show both counts in suggestions if they differ
|
|
444
|
+
count = effective_count
|
|
445
|
+
|
|
446
|
+
if count == 12:
|
|
447
|
+
return "quadruped"
|
|
448
|
+
elif count in (13, 14, 15):
|
|
449
|
+
return "quadruped_with_arm" # 12 leg + 1-3 arm servos
|
|
450
|
+
elif count == 6:
|
|
451
|
+
return "6dof_arm"
|
|
452
|
+
elif count == 18:
|
|
453
|
+
return "hexapod"
|
|
454
|
+
elif count in (16, 17):
|
|
455
|
+
return "humanoid_basic"
|
|
456
|
+
elif count in (18, 19, 20):
|
|
457
|
+
return "humanoid"
|
|
458
|
+
elif count in (21, 22, 23, 24):
|
|
459
|
+
return "humanoid_advanced"
|
|
460
|
+
elif count >= 25:
|
|
461
|
+
return "humanoid_full"
|
|
462
|
+
elif count <= 4:
|
|
463
|
+
return "robotic_arm"
|
|
464
|
+
return "custom"
|
|
465
|
+
|
|
466
|
+
def suggest_labels(self, robot_type: str, servo_ids: List[int]) -> Dict[int, Dict[str, str]]:
|
|
467
|
+
"""Suggest labels for servos based on robot type."""
|
|
468
|
+
labels = {}
|
|
469
|
+
|
|
470
|
+
if robot_type == "quadruped":
|
|
471
|
+
# Standard quadruped mapping
|
|
472
|
+
leg_names = ["front_right", "front_left", "back_right", "back_left"]
|
|
473
|
+
joint_names = ["hip", "upper", "lower"]
|
|
474
|
+
|
|
475
|
+
for i, servo_id in enumerate(sorted(servo_ids)):
|
|
476
|
+
leg_idx = i // 3
|
|
477
|
+
joint_idx = i % 3
|
|
478
|
+
|
|
479
|
+
if leg_idx < len(leg_names) and joint_idx < len(joint_names):
|
|
480
|
+
labels[servo_id] = {
|
|
481
|
+
"label": f"{leg_names[leg_idx]}_{joint_names[joint_idx]}",
|
|
482
|
+
"group": leg_names[leg_idx] + "_leg",
|
|
483
|
+
"role": joint_names[joint_idx]
|
|
484
|
+
}
|
|
485
|
+
else:
|
|
486
|
+
labels[servo_id] = {
|
|
487
|
+
"label": f"servo_{servo_id}",
|
|
488
|
+
"group": "other",
|
|
489
|
+
"role": "unknown"
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
elif robot_type == "hexapod":
|
|
493
|
+
leg_names = ["front_right", "middle_right", "back_right",
|
|
494
|
+
"back_left", "middle_left", "front_left"]
|
|
495
|
+
joint_names = ["coxa", "femur", "tibia"]
|
|
496
|
+
|
|
497
|
+
for i, servo_id in enumerate(sorted(servo_ids)):
|
|
498
|
+
leg_idx = i // 3
|
|
499
|
+
joint_idx = i % 3
|
|
500
|
+
|
|
501
|
+
if leg_idx < len(leg_names) and joint_idx < len(joint_names):
|
|
502
|
+
labels[servo_id] = {
|
|
503
|
+
"label": f"{leg_names[leg_idx]}_{joint_names[joint_idx]}",
|
|
504
|
+
"group": leg_names[leg_idx] + "_leg",
|
|
505
|
+
"role": joint_names[joint_idx]
|
|
506
|
+
}
|
|
507
|
+
else:
|
|
508
|
+
labels[servo_id] = {
|
|
509
|
+
"label": f"servo_{servo_id}",
|
|
510
|
+
"group": "other",
|
|
511
|
+
"role": "unknown"
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
elif robot_type == "quadruped_with_arm":
|
|
515
|
+
# 12 leg servos + up to 3 arm servos
|
|
516
|
+
leg_names = ["front_right", "front_left", "back_right", "back_left"]
|
|
517
|
+
joint_names = ["hip", "upper", "lower"]
|
|
518
|
+
arm_joints = ["shoulder", "elbow", "gripper"]
|
|
519
|
+
|
|
520
|
+
sorted_ids = sorted(servo_ids)
|
|
521
|
+
for i, servo_id in enumerate(sorted_ids):
|
|
522
|
+
if i < 12:
|
|
523
|
+
# Leg servos
|
|
524
|
+
leg_idx = i // 3
|
|
525
|
+
joint_idx = i % 3
|
|
526
|
+
labels[servo_id] = {
|
|
527
|
+
"label": f"{leg_names[leg_idx]}_{joint_names[joint_idx]}",
|
|
528
|
+
"group": leg_names[leg_idx] + "_leg",
|
|
529
|
+
"role": joint_names[joint_idx]
|
|
530
|
+
}
|
|
531
|
+
elif i < 12 + len(arm_joints):
|
|
532
|
+
# Arm servos
|
|
533
|
+
arm_idx = i - 12
|
|
534
|
+
labels[servo_id] = {
|
|
535
|
+
"label": f"arm_{arm_joints[arm_idx]}",
|
|
536
|
+
"group": "arm",
|
|
537
|
+
"role": arm_joints[arm_idx]
|
|
538
|
+
}
|
|
539
|
+
else:
|
|
540
|
+
labels[servo_id] = {
|
|
541
|
+
"label": f"servo_{servo_id}",
|
|
542
|
+
"group": "other",
|
|
543
|
+
"role": "unknown"
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
elif robot_type in ("humanoid_basic", "humanoid", "humanoid_advanced", "humanoid_full"):
|
|
547
|
+
# Humanoid labeling - organized by body part
|
|
548
|
+
# Standard humanoid servo layout (varies by manufacturer):
|
|
549
|
+
# - Right arm: shoulder_pitch, shoulder_roll, elbow
|
|
550
|
+
# - Left arm: shoulder_pitch, shoulder_roll, elbow
|
|
551
|
+
# - Right leg: hip_yaw, hip_roll, hip_pitch, knee, ankle_pitch, ankle_roll
|
|
552
|
+
# - Left leg: hip_yaw, hip_roll, hip_pitch, knee, ankle_pitch, ankle_roll
|
|
553
|
+
# - Torso: torso_yaw (optional)
|
|
554
|
+
# - Head: head_pan, head_tilt (optional)
|
|
555
|
+
|
|
556
|
+
sorted_ids = sorted(servo_ids)
|
|
557
|
+
num_servos = len(sorted_ids)
|
|
558
|
+
|
|
559
|
+
# Define humanoid body parts based on servo count
|
|
560
|
+
if robot_type == "humanoid_basic":
|
|
561
|
+
# 16-17 servos: basic arms (3 each) + legs (5 each)
|
|
562
|
+
body_map = [
|
|
563
|
+
# Right arm (3)
|
|
564
|
+
("right_shoulder_pitch", "right_arm", "shoulder_pitch"),
|
|
565
|
+
("right_shoulder_roll", "right_arm", "shoulder_roll"),
|
|
566
|
+
("right_elbow", "right_arm", "elbow"),
|
|
567
|
+
# Left arm (3)
|
|
568
|
+
("left_shoulder_pitch", "left_arm", "shoulder_pitch"),
|
|
569
|
+
("left_shoulder_roll", "left_arm", "shoulder_roll"),
|
|
570
|
+
("left_elbow", "left_arm", "elbow"),
|
|
571
|
+
# Right leg (5)
|
|
572
|
+
("right_hip_roll", "right_leg", "hip_roll"),
|
|
573
|
+
("right_hip_pitch", "right_leg", "hip_pitch"),
|
|
574
|
+
("right_knee", "right_leg", "knee"),
|
|
575
|
+
("right_ankle_pitch", "right_leg", "ankle_pitch"),
|
|
576
|
+
("right_ankle_roll", "right_leg", "ankle_roll"),
|
|
577
|
+
# Left leg (5)
|
|
578
|
+
("left_hip_roll", "left_leg", "hip_roll"),
|
|
579
|
+
("left_hip_pitch", "left_leg", "hip_pitch"),
|
|
580
|
+
("left_knee", "left_leg", "knee"),
|
|
581
|
+
("left_ankle_pitch", "left_leg", "ankle_pitch"),
|
|
582
|
+
("left_ankle_roll", "left_leg", "ankle_roll"),
|
|
583
|
+
# Extra (1)
|
|
584
|
+
("torso_yaw", "torso", "yaw"),
|
|
585
|
+
]
|
|
586
|
+
elif robot_type == "humanoid":
|
|
587
|
+
# 18-20 servos: arms (3 each) + legs (6 each) + torso
|
|
588
|
+
body_map = [
|
|
589
|
+
# Right arm (3)
|
|
590
|
+
("right_shoulder_pitch", "right_arm", "shoulder_pitch"),
|
|
591
|
+
("right_shoulder_roll", "right_arm", "shoulder_roll"),
|
|
592
|
+
("right_elbow", "right_arm", "elbow"),
|
|
593
|
+
# Left arm (3)
|
|
594
|
+
("left_shoulder_pitch", "left_arm", "shoulder_pitch"),
|
|
595
|
+
("left_shoulder_roll", "left_arm", "shoulder_roll"),
|
|
596
|
+
("left_elbow", "left_arm", "elbow"),
|
|
597
|
+
# Right leg (6)
|
|
598
|
+
("right_hip_yaw", "right_leg", "hip_yaw"),
|
|
599
|
+
("right_hip_roll", "right_leg", "hip_roll"),
|
|
600
|
+
("right_hip_pitch", "right_leg", "hip_pitch"),
|
|
601
|
+
("right_knee", "right_leg", "knee"),
|
|
602
|
+
("right_ankle_pitch", "right_leg", "ankle_pitch"),
|
|
603
|
+
("right_ankle_roll", "right_leg", "ankle_roll"),
|
|
604
|
+
# Left leg (6)
|
|
605
|
+
("left_hip_yaw", "left_leg", "hip_yaw"),
|
|
606
|
+
("left_hip_roll", "left_leg", "hip_roll"),
|
|
607
|
+
("left_hip_pitch", "left_leg", "hip_pitch"),
|
|
608
|
+
("left_knee", "left_leg", "knee"),
|
|
609
|
+
("left_ankle_pitch", "left_leg", "ankle_pitch"),
|
|
610
|
+
("left_ankle_roll", "left_leg", "ankle_roll"),
|
|
611
|
+
# Torso (2)
|
|
612
|
+
("torso_yaw", "torso", "yaw"),
|
|
613
|
+
("torso_pitch", "torso", "pitch"),
|
|
614
|
+
]
|
|
615
|
+
elif robot_type == "humanoid_advanced":
|
|
616
|
+
# 21-24 servos: full arms (4 each) + legs (6 each) + torso + head
|
|
617
|
+
body_map = [
|
|
618
|
+
# Right arm (4)
|
|
619
|
+
("right_shoulder_pitch", "right_arm", "shoulder_pitch"),
|
|
620
|
+
("right_shoulder_roll", "right_arm", "shoulder_roll"),
|
|
621
|
+
("right_shoulder_yaw", "right_arm", "shoulder_yaw"),
|
|
622
|
+
("right_elbow", "right_arm", "elbow"),
|
|
623
|
+
# Left arm (4)
|
|
624
|
+
("left_shoulder_pitch", "left_arm", "shoulder_pitch"),
|
|
625
|
+
("left_shoulder_roll", "left_arm", "shoulder_roll"),
|
|
626
|
+
("left_shoulder_yaw", "left_arm", "shoulder_yaw"),
|
|
627
|
+
("left_elbow", "left_arm", "elbow"),
|
|
628
|
+
# Right leg (6)
|
|
629
|
+
("right_hip_yaw", "right_leg", "hip_yaw"),
|
|
630
|
+
("right_hip_roll", "right_leg", "hip_roll"),
|
|
631
|
+
("right_hip_pitch", "right_leg", "hip_pitch"),
|
|
632
|
+
("right_knee", "right_leg", "knee"),
|
|
633
|
+
("right_ankle_pitch", "right_leg", "ankle_pitch"),
|
|
634
|
+
("right_ankle_roll", "right_leg", "ankle_roll"),
|
|
635
|
+
# Left leg (6)
|
|
636
|
+
("left_hip_yaw", "left_leg", "hip_yaw"),
|
|
637
|
+
("left_hip_roll", "left_leg", "hip_roll"),
|
|
638
|
+
("left_hip_pitch", "left_leg", "hip_pitch"),
|
|
639
|
+
("left_knee", "left_leg", "knee"),
|
|
640
|
+
("left_ankle_pitch", "left_leg", "ankle_pitch"),
|
|
641
|
+
("left_ankle_roll", "left_leg", "ankle_roll"),
|
|
642
|
+
# Head (2)
|
|
643
|
+
("head_pan", "head", "pan"),
|
|
644
|
+
("head_tilt", "head", "tilt"),
|
|
645
|
+
# Torso (2)
|
|
646
|
+
("torso_yaw", "torso", "yaw"),
|
|
647
|
+
("torso_pitch", "torso", "pitch"),
|
|
648
|
+
]
|
|
649
|
+
else: # humanoid_full
|
|
650
|
+
# 25+ servos: full body with wrists/grippers
|
|
651
|
+
body_map = [
|
|
652
|
+
# Right arm (6)
|
|
653
|
+
("right_shoulder_pitch", "right_arm", "shoulder_pitch"),
|
|
654
|
+
("right_shoulder_roll", "right_arm", "shoulder_roll"),
|
|
655
|
+
("right_shoulder_yaw", "right_arm", "shoulder_yaw"),
|
|
656
|
+
("right_elbow", "right_arm", "elbow"),
|
|
657
|
+
("right_wrist_yaw", "right_arm", "wrist_yaw"),
|
|
658
|
+
("right_gripper", "right_arm", "gripper"),
|
|
659
|
+
# Left arm (6)
|
|
660
|
+
("left_shoulder_pitch", "left_arm", "shoulder_pitch"),
|
|
661
|
+
("left_shoulder_roll", "left_arm", "shoulder_roll"),
|
|
662
|
+
("left_shoulder_yaw", "left_arm", "shoulder_yaw"),
|
|
663
|
+
("left_elbow", "left_arm", "elbow"),
|
|
664
|
+
("left_wrist_yaw", "left_arm", "wrist_yaw"),
|
|
665
|
+
("left_gripper", "left_arm", "gripper"),
|
|
666
|
+
# Right leg (6)
|
|
667
|
+
("right_hip_yaw", "right_leg", "hip_yaw"),
|
|
668
|
+
("right_hip_roll", "right_leg", "hip_roll"),
|
|
669
|
+
("right_hip_pitch", "right_leg", "hip_pitch"),
|
|
670
|
+
("right_knee", "right_leg", "knee"),
|
|
671
|
+
("right_ankle_pitch", "right_leg", "ankle_pitch"),
|
|
672
|
+
("right_ankle_roll", "right_leg", "ankle_roll"),
|
|
673
|
+
# Left leg (6)
|
|
674
|
+
("left_hip_yaw", "left_leg", "hip_yaw"),
|
|
675
|
+
("left_hip_roll", "left_leg", "hip_roll"),
|
|
676
|
+
("left_hip_pitch", "left_leg", "hip_pitch"),
|
|
677
|
+
("left_knee", "left_leg", "knee"),
|
|
678
|
+
("left_ankle_pitch", "left_leg", "ankle_pitch"),
|
|
679
|
+
("left_ankle_roll", "left_leg", "ankle_roll"),
|
|
680
|
+
# Head (2)
|
|
681
|
+
("head_pan", "head", "pan"),
|
|
682
|
+
("head_tilt", "head", "tilt"),
|
|
683
|
+
# Torso (2)
|
|
684
|
+
("torso_yaw", "torso", "yaw"),
|
|
685
|
+
("torso_pitch", "torso", "pitch"),
|
|
686
|
+
# Extra slots for variations
|
|
687
|
+
("head_roll", "head", "roll"),
|
|
688
|
+
("waist_yaw", "torso", "waist_yaw"),
|
|
689
|
+
]
|
|
690
|
+
|
|
691
|
+
# Apply labels
|
|
692
|
+
for i, servo_id in enumerate(sorted_ids):
|
|
693
|
+
if i < len(body_map):
|
|
694
|
+
label, group, role = body_map[i]
|
|
695
|
+
labels[servo_id] = {
|
|
696
|
+
"label": label,
|
|
697
|
+
"group": group,
|
|
698
|
+
"role": role
|
|
699
|
+
}
|
|
700
|
+
else:
|
|
701
|
+
labels[servo_id] = {
|
|
702
|
+
"label": f"extra_{servo_id}",
|
|
703
|
+
"group": "extra",
|
|
704
|
+
"role": "unknown"
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
elif robot_type == "6dof_arm":
|
|
708
|
+
# 6-DOF robotic arm
|
|
709
|
+
arm_joints = [
|
|
710
|
+
("base_rotation", "arm", "base"),
|
|
711
|
+
("shoulder_pitch", "arm", "shoulder"),
|
|
712
|
+
("elbow_pitch", "arm", "elbow"),
|
|
713
|
+
("wrist_pitch", "arm", "wrist_pitch"),
|
|
714
|
+
("wrist_roll", "arm", "wrist_roll"),
|
|
715
|
+
("gripper", "arm", "gripper"),
|
|
716
|
+
]
|
|
717
|
+
for i, servo_id in enumerate(sorted(servo_ids)):
|
|
718
|
+
if i < len(arm_joints):
|
|
719
|
+
label, group, role = arm_joints[i]
|
|
720
|
+
labels[servo_id] = {"label": label, "group": group, "role": role}
|
|
721
|
+
else:
|
|
722
|
+
labels[servo_id] = {"label": f"servo_{servo_id}", "group": "extra", "role": "unknown"}
|
|
723
|
+
|
|
724
|
+
else:
|
|
725
|
+
# Generic numbering for custom/unknown types
|
|
726
|
+
for servo_id in servo_ids:
|
|
727
|
+
labels[servo_id] = {
|
|
728
|
+
"label": f"servo_{servo_id}",
|
|
729
|
+
"group": "main",
|
|
730
|
+
"role": "actuator"
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return labels
|
|
734
|
+
|
|
735
|
+
def interactive_label_session(self,
|
|
736
|
+
robot_profile: RobotProfile,
|
|
737
|
+
protocol_handler: ServoProtocolHandler) -> RobotProfile:
|
|
738
|
+
"""Run interactive labeling session with AI assistance."""
|
|
739
|
+
|
|
740
|
+
WizardUI.print_header("AI-Assisted Component Labeling")
|
|
741
|
+
|
|
742
|
+
# Suggest robot type based on servo count and ID distribution
|
|
743
|
+
servo_ids = [a.id for a in robot_profile.actuators]
|
|
744
|
+
suggested_type = self.suggest_robot_type(len(servo_ids), servo_ids)
|
|
745
|
+
|
|
746
|
+
# Show detection info
|
|
747
|
+
core_count = self._count_core_servos(servo_ids)
|
|
748
|
+
if core_count < len(servo_ids):
|
|
749
|
+
WizardUI.print_info(f"Found {len(servo_ids)} servos total, {core_count} appear to be main actuators")
|
|
750
|
+
WizardUI.print_info(f"Detected robot type: {suggested_type}")
|
|
751
|
+
robot_type = WizardUI.prompt("Robot type", suggested_type)
|
|
752
|
+
robot_profile.robot_type = robot_type
|
|
753
|
+
|
|
754
|
+
# Get suggested labels
|
|
755
|
+
suggested_labels = self.suggest_labels(robot_type, servo_ids)
|
|
756
|
+
|
|
757
|
+
# Separate core and peripheral servos for display
|
|
758
|
+
core_servo_ids = [id for id in servo_ids if id <= 15 or suggested_labels.get(id, {}).get('group') != 'other']
|
|
759
|
+
peripheral_servo_ids = [id for id in servo_ids if id not in core_servo_ids]
|
|
760
|
+
|
|
761
|
+
print("\nSuggested servo labels (main actuators):")
|
|
762
|
+
print("-" * 50)
|
|
763
|
+
|
|
764
|
+
for servo_id in sorted(core_servo_ids):
|
|
765
|
+
if servo_id in suggested_labels:
|
|
766
|
+
s = suggested_labels[servo_id]
|
|
767
|
+
print(f" Servo {servo_id:2d}: {s['label']} ({s['group']}/{s['role']})")
|
|
768
|
+
|
|
769
|
+
if peripheral_servo_ids:
|
|
770
|
+
print(f"\n + {len(peripheral_servo_ids)} peripheral servos (IDs: {min(peripheral_servo_ids)}-{max(peripheral_servo_ids)})")
|
|
771
|
+
|
|
772
|
+
# Offer three options
|
|
773
|
+
print("\nHow would you like to proceed?")
|
|
774
|
+
options = [
|
|
775
|
+
"Accept suggested labels",
|
|
776
|
+
"🔍 Verify interactively (wiggle each servo to confirm)",
|
|
777
|
+
"Label manually from scratch"
|
|
778
|
+
]
|
|
779
|
+
choice = WizardUI.select("Choose an option:", options)
|
|
780
|
+
|
|
781
|
+
if not choice:
|
|
782
|
+
choice = [0] # Default to accept
|
|
783
|
+
|
|
784
|
+
if choice[0] == 0:
|
|
785
|
+
# Accept suggested labels
|
|
786
|
+
for actuator in robot_profile.actuators:
|
|
787
|
+
if actuator.id in suggested_labels:
|
|
788
|
+
actuator.label = suggested_labels[actuator.id]['label']
|
|
789
|
+
actuator.group = suggested_labels[actuator.id]['group']
|
|
790
|
+
actuator.role = suggested_labels[actuator.id]['role']
|
|
791
|
+
|
|
792
|
+
elif choice[0] == 1:
|
|
793
|
+
# Interactive verification - wiggle each core servo
|
|
794
|
+
WizardUI.print_info("Interactive verification mode")
|
|
795
|
+
WizardUI.print_info("I'll wiggle each servo - press Enter to confirm or type a new label\n")
|
|
796
|
+
|
|
797
|
+
# Group actuators by suggested group for organized verification
|
|
798
|
+
groups_order = ['front_right_leg', 'front_left_leg', 'back_right_leg', 'back_left_leg', 'arm', 'other']
|
|
799
|
+
actuators_by_group = {}
|
|
800
|
+
for actuator in robot_profile.actuators:
|
|
801
|
+
group = suggested_labels.get(actuator.id, {}).get('group', 'other')
|
|
802
|
+
if group not in actuators_by_group:
|
|
803
|
+
actuators_by_group[group] = []
|
|
804
|
+
actuators_by_group[group].append(actuator)
|
|
805
|
+
|
|
806
|
+
for group in groups_order:
|
|
807
|
+
if group not in actuators_by_group:
|
|
808
|
+
continue
|
|
809
|
+
|
|
810
|
+
actuators = actuators_by_group[group]
|
|
811
|
+
if group == 'other' and len(actuators) > 5:
|
|
812
|
+
# Skip bulk peripheral servos
|
|
813
|
+
WizardUI.print_info(f"\nSkipping {len(actuators)} peripheral servos in '{group}' group")
|
|
814
|
+
for actuator in actuators:
|
|
815
|
+
actuator.label = f"servo_{actuator.id}"
|
|
816
|
+
actuator.group = "other"
|
|
817
|
+
actuator.role = "unknown"
|
|
818
|
+
continue
|
|
819
|
+
|
|
820
|
+
print(f"\n--- {group.replace('_', ' ').title()} ---")
|
|
821
|
+
|
|
822
|
+
for actuator in actuators:
|
|
823
|
+
suggestion = suggested_labels.get(actuator.id, {})
|
|
824
|
+
default_label = suggestion.get('label', f'servo_{actuator.id}')
|
|
825
|
+
|
|
826
|
+
# Wiggle the servo
|
|
827
|
+
print(f"\n Servo {actuator.id}: Wiggling... ", end='', flush=True)
|
|
828
|
+
try:
|
|
829
|
+
current_pos = protocol_handler.read_position(actuator.id) or 500
|
|
830
|
+
protocol_handler.move_servo(actuator.id, current_pos + 60, 150)
|
|
831
|
+
time.sleep(0.2)
|
|
832
|
+
protocol_handler.move_servo(actuator.id, current_pos - 60, 150)
|
|
833
|
+
time.sleep(0.2)
|
|
834
|
+
protocol_handler.move_servo(actuator.id, current_pos, 150)
|
|
835
|
+
time.sleep(0.2)
|
|
836
|
+
print("done")
|
|
837
|
+
except Exception as e:
|
|
838
|
+
print(f"(error: {e})")
|
|
839
|
+
|
|
840
|
+
# Ask for confirmation
|
|
841
|
+
user_label = WizardUI.prompt(f" Label [{default_label}]", "")
|
|
842
|
+
actuator.label = user_label if user_label else default_label
|
|
843
|
+
actuator.group = suggestion.get('group', 'main')
|
|
844
|
+
actuator.role = suggestion.get('role', 'actuator')
|
|
845
|
+
|
|
846
|
+
else:
|
|
847
|
+
# Full manual labeling
|
|
848
|
+
WizardUI.print_info("Manual labeling mode")
|
|
849
|
+
WizardUI.print_info("I'll wiggle each servo so you can identify and label it.\n")
|
|
850
|
+
|
|
851
|
+
for actuator in robot_profile.actuators:
|
|
852
|
+
print(f"\n--- Servo ID {actuator.id} ---")
|
|
853
|
+
|
|
854
|
+
if WizardUI.confirm("Wiggle this servo?", default=True):
|
|
855
|
+
try:
|
|
856
|
+
current_pos = protocol_handler.read_position(actuator.id) or 500
|
|
857
|
+
protocol_handler.move_servo(actuator.id, current_pos + 50, 200)
|
|
858
|
+
time.sleep(0.3)
|
|
859
|
+
protocol_handler.move_servo(actuator.id, current_pos - 50, 200)
|
|
860
|
+
time.sleep(0.3)
|
|
861
|
+
protocol_handler.move_servo(actuator.id, current_pos, 200)
|
|
862
|
+
time.sleep(0.3)
|
|
863
|
+
except Exception:
|
|
864
|
+
pass
|
|
865
|
+
|
|
866
|
+
default_label = suggested_labels.get(actuator.id, {}).get('label', f'servo_{actuator.id}')
|
|
867
|
+
actuator.label = WizardUI.prompt("Label", default_label)
|
|
868
|
+
actuator.group = WizardUI.prompt("Group (e.g., left_leg, arm)",
|
|
869
|
+
suggested_labels.get(actuator.id, {}).get('group', 'main'))
|
|
870
|
+
actuator.role = WizardUI.prompt("Role (e.g., hip, knee, shoulder)",
|
|
871
|
+
suggested_labels.get(actuator.id, {}).get('role', 'actuator'))
|
|
872
|
+
|
|
873
|
+
# Build groups
|
|
874
|
+
robot_profile.groups = {}
|
|
875
|
+
for actuator in robot_profile.actuators:
|
|
876
|
+
if actuator.group not in robot_profile.groups:
|
|
877
|
+
robot_profile.groups[actuator.group] = []
|
|
878
|
+
robot_profile.groups[actuator.group].append(actuator.id)
|
|
879
|
+
|
|
880
|
+
return robot_profile
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class PrimitiveSkillGenerator:
|
|
884
|
+
"""Generates Python code and JSON definitions for primitive skills."""
|
|
885
|
+
|
|
886
|
+
def __init__(self, robot_profile: RobotProfile):
|
|
887
|
+
self.profile = robot_profile
|
|
888
|
+
self.generated_primitives: List[Dict[str, Any]] = [] # Track for batch push
|
|
889
|
+
|
|
890
|
+
def generate_protocol_definition(self) -> Dict[str, Any]:
|
|
891
|
+
"""Generate protocol.json definition."""
|
|
892
|
+
return {
|
|
893
|
+
"version": "1.0",
|
|
894
|
+
"robot_model": self.profile.name,
|
|
895
|
+
"robot_type": self.profile.robot_type,
|
|
896
|
+
"manufacturer": self.profile.manufacturer,
|
|
897
|
+
"model": self.profile.model,
|
|
898
|
+
"transport": {
|
|
899
|
+
"type": "serial",
|
|
900
|
+
"port": self.profile.device.port if self.profile.device else "",
|
|
901
|
+
"baud_rate": self.profile.device.baud_rate if self.profile.device else 115200,
|
|
902
|
+
"protocol": self.profile.device.detected_protocol.value if self.profile.device and self.profile.device.detected_protocol else "hiwonder"
|
|
903
|
+
},
|
|
904
|
+
"actuators": [
|
|
905
|
+
{
|
|
906
|
+
"id": a.id,
|
|
907
|
+
"label": a.label,
|
|
908
|
+
"group": a.group,
|
|
909
|
+
"role": a.role,
|
|
910
|
+
"type": a.actuator_type.value,
|
|
911
|
+
"characteristics": asdict(a.characteristics)
|
|
912
|
+
}
|
|
913
|
+
for a in self.profile.actuators
|
|
914
|
+
],
|
|
915
|
+
"groups": self.profile.groups,
|
|
916
|
+
"created_at": self.profile.created_at,
|
|
917
|
+
"wizard_version": self.profile.wizard_version
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
def generate_servo_map(self) -> str:
|
|
921
|
+
"""Generate Python servo map code."""
|
|
922
|
+
lines = [
|
|
923
|
+
'"""',
|
|
924
|
+
f'Servo map for {self.profile.name}',
|
|
925
|
+
f'Generated by ate robot-setup wizard',
|
|
926
|
+
f'Robot type: {self.profile.robot_type}',
|
|
927
|
+
'"""',
|
|
928
|
+
'',
|
|
929
|
+
'from enum import IntEnum',
|
|
930
|
+
'',
|
|
931
|
+
'',
|
|
932
|
+
'class ServoID(IntEnum):',
|
|
933
|
+
' """Servo IDs mapped to descriptive names."""',
|
|
934
|
+
]
|
|
935
|
+
|
|
936
|
+
for actuator in sorted(self.profile.actuators, key=lambda a: a.id):
|
|
937
|
+
name = actuator.label.upper().replace(' ', '_').replace('-', '_')
|
|
938
|
+
lines.append(f' {name} = {actuator.id}')
|
|
939
|
+
|
|
940
|
+
lines.extend([
|
|
941
|
+
'',
|
|
942
|
+
'',
|
|
943
|
+
'# Servo groups',
|
|
944
|
+
])
|
|
945
|
+
|
|
946
|
+
for group_name, servo_ids in self.profile.groups.items():
|
|
947
|
+
var_name = group_name.upper().replace(' ', '_').replace('-', '_')
|
|
948
|
+
servo_names = [f'ServoID.{self.profile.actuators[i].label.upper().replace(" ", "_").replace("-", "_")}'
|
|
949
|
+
for i, a in enumerate(self.profile.actuators) if a.id in servo_ids]
|
|
950
|
+
lines.append(f'{var_name} = [{", ".join(servo_names)}]')
|
|
951
|
+
|
|
952
|
+
lines.extend([
|
|
953
|
+
'',
|
|
954
|
+
'',
|
|
955
|
+
'# All servos',
|
|
956
|
+
f'ALL_SERVOS = list(ServoID)',
|
|
957
|
+
'',
|
|
958
|
+
'',
|
|
959
|
+
'# Servo characteristics',
|
|
960
|
+
'SERVO_LIMITS = {',
|
|
961
|
+
])
|
|
962
|
+
|
|
963
|
+
for actuator in self.profile.actuators:
|
|
964
|
+
chars = actuator.characteristics
|
|
965
|
+
lines.append(f' ServoID.{actuator.label.upper().replace(" ", "_").replace("-", "_")}: {{')
|
|
966
|
+
lines.append(f' "min": {chars.position_min},')
|
|
967
|
+
lines.append(f' "max": {chars.position_max},')
|
|
968
|
+
lines.append(f' "center": {chars.position_center},')
|
|
969
|
+
lines.append(f' }},')
|
|
970
|
+
|
|
971
|
+
lines.append('}')
|
|
972
|
+
|
|
973
|
+
return '\n'.join(lines)
|
|
974
|
+
|
|
975
|
+
def generate_primitives(self) -> str:
|
|
976
|
+
"""Generate primitive skill code."""
|
|
977
|
+
lines = [
|
|
978
|
+
'"""',
|
|
979
|
+
f'Primitive skills for {self.profile.name}',
|
|
980
|
+
f'Generated by ate robot-setup wizard',
|
|
981
|
+
'"""',
|
|
982
|
+
'',
|
|
983
|
+
'import time',
|
|
984
|
+
'from typing import List, Dict, Optional',
|
|
985
|
+
'from dataclasses import dataclass',
|
|
986
|
+
'',
|
|
987
|
+
'from .servo_map import ServoID, SERVO_LIMITS, ALL_SERVOS',
|
|
988
|
+
'',
|
|
989
|
+
'',
|
|
990
|
+
'@dataclass',
|
|
991
|
+
'class ServoCommand:',
|
|
992
|
+
' """Command for a single servo."""',
|
|
993
|
+
' servo_id: int',
|
|
994
|
+
' position: int',
|
|
995
|
+
' time_ms: int = 500',
|
|
996
|
+
'',
|
|
997
|
+
'',
|
|
998
|
+
'class PrimitiveSkills:',
|
|
999
|
+
' """Low-level primitive skills for the robot."""',
|
|
1000
|
+
'',
|
|
1001
|
+
' def __init__(self, robot_controller):',
|
|
1002
|
+
' """',
|
|
1003
|
+
' Args:',
|
|
1004
|
+
' robot_controller: Controller with move_servo(), read_position() methods',
|
|
1005
|
+
' """',
|
|
1006
|
+
' self.controller = robot_controller',
|
|
1007
|
+
'',
|
|
1008
|
+
' def move_servo(self, servo_id: ServoID, position: int, time_ms: int = 500):',
|
|
1009
|
+
' """Move a single servo to position."""',
|
|
1010
|
+
' limits = SERVO_LIMITS.get(servo_id, {"min": 0, "max": 1000})',
|
|
1011
|
+
' position = max(limits["min"], min(limits["max"], position))',
|
|
1012
|
+
' self.controller.move_servo(servo_id, position, time_ms)',
|
|
1013
|
+
'',
|
|
1014
|
+
' def move_servos(self, commands: List[ServoCommand]):',
|
|
1015
|
+
' """Move multiple servos simultaneously."""',
|
|
1016
|
+
' for cmd in commands:',
|
|
1017
|
+
' limits = SERVO_LIMITS.get(cmd.servo_id, {"min": 0, "max": 1000})',
|
|
1018
|
+
' pos = max(limits["min"], min(limits["max"], cmd.position))',
|
|
1019
|
+
' self.controller.move_servo(cmd.servo_id, pos, cmd.time_ms)',
|
|
1020
|
+
'',
|
|
1021
|
+
' def home(self):',
|
|
1022
|
+
' """Move all servos to center/home position."""',
|
|
1023
|
+
' commands = [',
|
|
1024
|
+
' ServoCommand(servo_id, SERVO_LIMITS[servo_id]["center"])',
|
|
1025
|
+
' for servo_id in ALL_SERVOS',
|
|
1026
|
+
' ]',
|
|
1027
|
+
' self.move_servos(commands)',
|
|
1028
|
+
'',
|
|
1029
|
+
' def read_positions(self) -> Dict[ServoID, int]:',
|
|
1030
|
+
' """Read all servo positions."""',
|
|
1031
|
+
' positions = {}',
|
|
1032
|
+
' for servo_id in ALL_SERVOS:',
|
|
1033
|
+
' pos = self.controller.read_position(servo_id)',
|
|
1034
|
+
' if pos is not None:',
|
|
1035
|
+
' positions[servo_id] = pos',
|
|
1036
|
+
' return positions',
|
|
1037
|
+
'',
|
|
1038
|
+
]
|
|
1039
|
+
|
|
1040
|
+
# Add group-specific methods
|
|
1041
|
+
for group_name, servo_ids in self.profile.groups.items():
|
|
1042
|
+
method_name = group_name.lower().replace(' ', '_').replace('-', '_')
|
|
1043
|
+
group_var = group_name.upper().replace(' ', '_').replace('-', '_')
|
|
1044
|
+
|
|
1045
|
+
lines.extend([
|
|
1046
|
+
f' def move_{method_name}(self, positions: Dict[ServoID, int], time_ms: int = 500):',
|
|
1047
|
+
f' """Move {group_name} servos."""',
|
|
1048
|
+
f' commands = [',
|
|
1049
|
+
f' ServoCommand(servo_id, pos, time_ms)',
|
|
1050
|
+
f' for servo_id, pos in positions.items()',
|
|
1051
|
+
f' if servo_id in {group_var}',
|
|
1052
|
+
f' ]',
|
|
1053
|
+
f' self.move_servos(commands)',
|
|
1054
|
+
'',
|
|
1055
|
+
])
|
|
1056
|
+
|
|
1057
|
+
return '\n'.join(lines)
|
|
1058
|
+
|
|
1059
|
+
def generate_primitive_jsons(self, output_dir: Path) -> List[str]:
|
|
1060
|
+
"""Generate .primitive.json files for CLI push workflow.
|
|
1061
|
+
|
|
1062
|
+
This is the PostHog-style auto-generation - from discovered hardware
|
|
1063
|
+
we automatically create a comprehensive library of primitive skills.
|
|
1064
|
+
"""
|
|
1065
|
+
primitives_dir = output_dir / "primitives"
|
|
1066
|
+
primitives_dir.mkdir(parents=True, exist_ok=True)
|
|
1067
|
+
|
|
1068
|
+
generated_files = []
|
|
1069
|
+
self.generated_primitives = []
|
|
1070
|
+
|
|
1071
|
+
device = self.profile.device
|
|
1072
|
+
protocol_type = device.detected_protocol.value if device and device.detected_protocol else "hiwonder"
|
|
1073
|
+
baud_rate = device.baud_rate if device else 115200
|
|
1074
|
+
|
|
1075
|
+
# ====================================================================
|
|
1076
|
+
# 1. Per-Servo Primitives - Direct control of each labeled actuator
|
|
1077
|
+
# ====================================================================
|
|
1078
|
+
for actuator in self.profile.actuators:
|
|
1079
|
+
chars = actuator.characteristics
|
|
1080
|
+
|
|
1081
|
+
# move_<label>(position, speed) - Move servo to absolute position
|
|
1082
|
+
primitive = {
|
|
1083
|
+
"name": f"move_{actuator.label}",
|
|
1084
|
+
"description": f"Move {actuator.label.replace('_', ' ')} servo to specified position",
|
|
1085
|
+
"category": "motion",
|
|
1086
|
+
"commandType": "single",
|
|
1087
|
+
"commandTemplate": self._build_move_command(actuator.id, protocol_type),
|
|
1088
|
+
"parameters": {
|
|
1089
|
+
"position": {
|
|
1090
|
+
"type": "integer",
|
|
1091
|
+
"min": chars.position_min,
|
|
1092
|
+
"max": chars.position_max,
|
|
1093
|
+
"default": chars.position_center,
|
|
1094
|
+
"description": f"Target position ({chars.position_min}-{chars.position_max})"
|
|
1095
|
+
},
|
|
1096
|
+
"time_ms": {
|
|
1097
|
+
"type": "integer",
|
|
1098
|
+
"min": 0,
|
|
1099
|
+
"max": 5000,
|
|
1100
|
+
"default": 500,
|
|
1101
|
+
"description": "Movement duration in milliseconds"
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
"servoId": actuator.id,
|
|
1105
|
+
"group": actuator.group,
|
|
1106
|
+
"role": actuator.role,
|
|
1107
|
+
"protocol": {
|
|
1108
|
+
"type": protocol_type,
|
|
1109
|
+
"baudRate": baud_rate
|
|
1110
|
+
},
|
|
1111
|
+
"robotModel": self.profile.name,
|
|
1112
|
+
"robotType": self.profile.robot_type,
|
|
1113
|
+
"executionTimeMs": 500,
|
|
1114
|
+
"settleTimeMs": 100,
|
|
1115
|
+
"safetyNotes": f"Ensure {actuator.label} has clearance before moving"
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
filename = f"move_{actuator.label}.primitive.json"
|
|
1119
|
+
filepath = primitives_dir / filename
|
|
1120
|
+
with open(filepath, 'w') as f:
|
|
1121
|
+
json.dump(primitive, f, indent=2)
|
|
1122
|
+
generated_files.append(str(filepath))
|
|
1123
|
+
self.generated_primitives.append(primitive)
|
|
1124
|
+
|
|
1125
|
+
# center_<label>() - Move to center/home position
|
|
1126
|
+
center_primitive = {
|
|
1127
|
+
"name": f"center_{actuator.label}",
|
|
1128
|
+
"description": f"Move {actuator.label.replace('_', ' ')} to center position",
|
|
1129
|
+
"category": "motion",
|
|
1130
|
+
"commandType": "single",
|
|
1131
|
+
"commandTemplate": self._build_move_command(actuator.id, protocol_type, chars.position_center),
|
|
1132
|
+
"parameters": {
|
|
1133
|
+
"time_ms": {
|
|
1134
|
+
"type": "integer",
|
|
1135
|
+
"min": 0,
|
|
1136
|
+
"max": 5000,
|
|
1137
|
+
"default": 500,
|
|
1138
|
+
"description": "Movement duration in milliseconds"
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
"servoId": actuator.id,
|
|
1142
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1143
|
+
"robotModel": self.profile.name,
|
|
1144
|
+
"robotType": self.profile.robot_type
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
filename = f"center_{actuator.label}.primitive.json"
|
|
1148
|
+
filepath = primitives_dir / filename
|
|
1149
|
+
with open(filepath, 'w') as f:
|
|
1150
|
+
json.dump(center_primitive, f, indent=2)
|
|
1151
|
+
generated_files.append(str(filepath))
|
|
1152
|
+
self.generated_primitives.append(center_primitive)
|
|
1153
|
+
|
|
1154
|
+
# ====================================================================
|
|
1155
|
+
# 2. Group Primitives - Coordinated control of actuator groups
|
|
1156
|
+
# ====================================================================
|
|
1157
|
+
for group_name, servo_ids in self.profile.groups.items():
|
|
1158
|
+
if group_name in ('other', 'extra'):
|
|
1159
|
+
continue # Skip misc groups
|
|
1160
|
+
|
|
1161
|
+
group_actuators = [a for a in self.profile.actuators if a.id in servo_ids]
|
|
1162
|
+
|
|
1163
|
+
# move_<group>(positions) - Move all servos in group
|
|
1164
|
+
group_primitive = {
|
|
1165
|
+
"name": f"move_{group_name}",
|
|
1166
|
+
"description": f"Move all servos in {group_name.replace('_', ' ')} group",
|
|
1167
|
+
"category": "motion",
|
|
1168
|
+
"commandType": "sequence",
|
|
1169
|
+
"commandTemplate": json.dumps([
|
|
1170
|
+
{"servoId": a.id, "position": "${" + a.role + "_position}", "time_ms": "${time_ms}"}
|
|
1171
|
+
for a in group_actuators
|
|
1172
|
+
]),
|
|
1173
|
+
"parameters": {
|
|
1174
|
+
**{
|
|
1175
|
+
f"{a.role}_position": {
|
|
1176
|
+
"type": "integer",
|
|
1177
|
+
"min": a.characteristics.position_min,
|
|
1178
|
+
"max": a.characteristics.position_max,
|
|
1179
|
+
"default": a.characteristics.position_center,
|
|
1180
|
+
"description": f"{a.role} servo position"
|
|
1181
|
+
}
|
|
1182
|
+
for a in group_actuators
|
|
1183
|
+
},
|
|
1184
|
+
"time_ms": {
|
|
1185
|
+
"type": "integer",
|
|
1186
|
+
"min": 0,
|
|
1187
|
+
"max": 5000,
|
|
1188
|
+
"default": 500,
|
|
1189
|
+
"description": "Movement duration"
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
"servoIds": servo_ids,
|
|
1193
|
+
"group": group_name,
|
|
1194
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1195
|
+
"robotModel": self.profile.name,
|
|
1196
|
+
"robotType": self.profile.robot_type,
|
|
1197
|
+
"executionTimeMs": 500,
|
|
1198
|
+
"settleTimeMs": 200
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
filename = f"move_{group_name}.primitive.json"
|
|
1202
|
+
filepath = primitives_dir / filename
|
|
1203
|
+
with open(filepath, 'w') as f:
|
|
1204
|
+
json.dump(group_primitive, f, indent=2)
|
|
1205
|
+
generated_files.append(str(filepath))
|
|
1206
|
+
self.generated_primitives.append(group_primitive)
|
|
1207
|
+
|
|
1208
|
+
# home_<group>() - Move all servos in group to center
|
|
1209
|
+
home_group = {
|
|
1210
|
+
"name": f"home_{group_name}",
|
|
1211
|
+
"description": f"Move all {group_name.replace('_', ' ')} servos to home position",
|
|
1212
|
+
"category": "motion",
|
|
1213
|
+
"commandType": "sequence",
|
|
1214
|
+
"commandTemplate": json.dumps([
|
|
1215
|
+
{"servoId": a.id, "position": a.characteristics.position_center, "time_ms": 500}
|
|
1216
|
+
for a in group_actuators
|
|
1217
|
+
]),
|
|
1218
|
+
"parameters": {},
|
|
1219
|
+
"servoIds": servo_ids,
|
|
1220
|
+
"group": group_name,
|
|
1221
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1222
|
+
"robotModel": self.profile.name,
|
|
1223
|
+
"robotType": self.profile.robot_type
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
filename = f"home_{group_name}.primitive.json"
|
|
1227
|
+
filepath = primitives_dir / filename
|
|
1228
|
+
with open(filepath, 'w') as f:
|
|
1229
|
+
json.dump(home_group, f, indent=2)
|
|
1230
|
+
generated_files.append(str(filepath))
|
|
1231
|
+
self.generated_primitives.append(home_group)
|
|
1232
|
+
|
|
1233
|
+
# ====================================================================
|
|
1234
|
+
# 3. Utility Primitives - System-wide control
|
|
1235
|
+
# ====================================================================
|
|
1236
|
+
all_servo_ids = [a.id for a in self.profile.actuators]
|
|
1237
|
+
|
|
1238
|
+
# home_all() - All servos to center
|
|
1239
|
+
home_all = {
|
|
1240
|
+
"name": "home_all",
|
|
1241
|
+
"description": "Move all servos to their center/home positions",
|
|
1242
|
+
"category": "motion",
|
|
1243
|
+
"commandType": "sequence",
|
|
1244
|
+
"commandTemplate": json.dumps([
|
|
1245
|
+
{"servoId": a.id, "position": a.characteristics.position_center, "time_ms": 800}
|
|
1246
|
+
for a in self.profile.actuators
|
|
1247
|
+
]),
|
|
1248
|
+
"parameters": {},
|
|
1249
|
+
"servoIds": all_servo_ids,
|
|
1250
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1251
|
+
"robotModel": self.profile.name,
|
|
1252
|
+
"robotType": self.profile.robot_type,
|
|
1253
|
+
"executionTimeMs": 1000,
|
|
1254
|
+
"settleTimeMs": 500,
|
|
1255
|
+
"safetyNotes": "Ensure robot has clearance. May cause significant movement."
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
filepath = primitives_dir / "home_all.primitive.json"
|
|
1259
|
+
with open(filepath, 'w') as f:
|
|
1260
|
+
json.dump(home_all, f, indent=2)
|
|
1261
|
+
generated_files.append(str(filepath))
|
|
1262
|
+
self.generated_primitives.append(home_all)
|
|
1263
|
+
|
|
1264
|
+
# disable_torque() - Release all servos
|
|
1265
|
+
disable_torque = {
|
|
1266
|
+
"name": "disable_torque",
|
|
1267
|
+
"description": "Disable torque on all servos (robot goes limp)",
|
|
1268
|
+
"category": "safety",
|
|
1269
|
+
"commandType": "sequence",
|
|
1270
|
+
"commandTemplate": json.dumps([
|
|
1271
|
+
{"servoId": a.id, "command": "disable_torque"}
|
|
1272
|
+
for a in self.profile.actuators
|
|
1273
|
+
]),
|
|
1274
|
+
"parameters": {},
|
|
1275
|
+
"servoIds": all_servo_ids,
|
|
1276
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1277
|
+
"robotModel": self.profile.name,
|
|
1278
|
+
"robotType": self.profile.robot_type,
|
|
1279
|
+
"safetyNotes": "Robot will go limp. Ensure it is supported."
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
filepath = primitives_dir / "disable_torque.primitive.json"
|
|
1283
|
+
with open(filepath, 'w') as f:
|
|
1284
|
+
json.dump(disable_torque, f, indent=2)
|
|
1285
|
+
generated_files.append(str(filepath))
|
|
1286
|
+
self.generated_primitives.append(disable_torque)
|
|
1287
|
+
|
|
1288
|
+
# enable_torque() - Enable all servos
|
|
1289
|
+
enable_torque = {
|
|
1290
|
+
"name": "enable_torque",
|
|
1291
|
+
"description": "Enable torque on all servos",
|
|
1292
|
+
"category": "safety",
|
|
1293
|
+
"commandType": "sequence",
|
|
1294
|
+
"commandTemplate": json.dumps([
|
|
1295
|
+
{"servoId": a.id, "command": "enable_torque"}
|
|
1296
|
+
for a in self.profile.actuators
|
|
1297
|
+
]),
|
|
1298
|
+
"parameters": {},
|
|
1299
|
+
"servoIds": all_servo_ids,
|
|
1300
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1301
|
+
"robotModel": self.profile.name,
|
|
1302
|
+
"robotType": self.profile.robot_type
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
filepath = primitives_dir / "enable_torque.primitive.json"
|
|
1306
|
+
with open(filepath, 'w') as f:
|
|
1307
|
+
json.dump(enable_torque, f, indent=2)
|
|
1308
|
+
generated_files.append(str(filepath))
|
|
1309
|
+
self.generated_primitives.append(enable_torque)
|
|
1310
|
+
|
|
1311
|
+
# read_all_positions() - Read state of all servos
|
|
1312
|
+
read_positions = {
|
|
1313
|
+
"name": "read_all_positions",
|
|
1314
|
+
"description": "Read current positions of all servos",
|
|
1315
|
+
"category": "sensing",
|
|
1316
|
+
"commandType": "sequence",
|
|
1317
|
+
"commandTemplate": json.dumps([
|
|
1318
|
+
{"servoId": a.id, "command": "read_position"}
|
|
1319
|
+
for a in self.profile.actuators
|
|
1320
|
+
]),
|
|
1321
|
+
"parameters": {},
|
|
1322
|
+
"servoIds": all_servo_ids,
|
|
1323
|
+
"protocol": {"type": protocol_type, "baudRate": baud_rate},
|
|
1324
|
+
"robotModel": self.profile.name,
|
|
1325
|
+
"robotType": self.profile.robot_type
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
filepath = primitives_dir / "read_all_positions.primitive.json"
|
|
1329
|
+
with open(filepath, 'w') as f:
|
|
1330
|
+
json.dump(read_positions, f, indent=2)
|
|
1331
|
+
generated_files.append(str(filepath))
|
|
1332
|
+
self.generated_primitives.append(read_positions)
|
|
1333
|
+
|
|
1334
|
+
# ====================================================================
|
|
1335
|
+
# 4. Robot-Type Specific Poses
|
|
1336
|
+
# ====================================================================
|
|
1337
|
+
generated_files.extend(self._generate_pose_primitives(primitives_dir, protocol_type, baud_rate))
|
|
1338
|
+
|
|
1339
|
+
return generated_files
|
|
1340
|
+
|
|
1341
|
+
def _build_move_command(self, servo_id: int, protocol: str, fixed_position: Optional[int] = None) -> str:
|
|
1342
|
+
"""Build command template for moving a servo."""
|
|
1343
|
+
if protocol == "hiwonder":
|
|
1344
|
+
if fixed_position is not None:
|
|
1345
|
+
return json.dumps({
|
|
1346
|
+
"servoId": servo_id,
|
|
1347
|
+
"position": fixed_position,
|
|
1348
|
+
"time_ms": "${time_ms}"
|
|
1349
|
+
})
|
|
1350
|
+
return json.dumps({
|
|
1351
|
+
"servoId": servo_id,
|
|
1352
|
+
"position": "${position}",
|
|
1353
|
+
"time_ms": "${time_ms}"
|
|
1354
|
+
})
|
|
1355
|
+
# Generic fallback
|
|
1356
|
+
if fixed_position is not None:
|
|
1357
|
+
return f"MOVE {servo_id} {fixed_position} ${{time_ms}}"
|
|
1358
|
+
return f"MOVE {servo_id} ${{position}} ${{time_ms}}"
|
|
1359
|
+
|
|
1360
|
+
def _generate_pose_primitives(self, output_dir: Path, protocol: str, baud_rate: int) -> List[str]:
|
|
1361
|
+
"""Generate robot-type specific pose primitives."""
|
|
1362
|
+
generated_files = []
|
|
1363
|
+
robot_type = self.profile.robot_type
|
|
1364
|
+
|
|
1365
|
+
if robot_type == "quadruped":
|
|
1366
|
+
poses = self._generate_quadruped_poses()
|
|
1367
|
+
elif robot_type in ("humanoid", "humanoid_basic", "humanoid_advanced", "humanoid_full"):
|
|
1368
|
+
poses = self._generate_humanoid_poses()
|
|
1369
|
+
elif robot_type == "6dof_arm":
|
|
1370
|
+
poses = self._generate_arm_poses()
|
|
1371
|
+
else:
|
|
1372
|
+
poses = []
|
|
1373
|
+
|
|
1374
|
+
for pose in poses:
|
|
1375
|
+
pose["protocol"] = {"type": protocol, "baudRate": baud_rate}
|
|
1376
|
+
pose["robotModel"] = self.profile.name
|
|
1377
|
+
pose["robotType"] = self.profile.robot_type
|
|
1378
|
+
|
|
1379
|
+
filename = f"{pose['name']}.primitive.json"
|
|
1380
|
+
filepath = output_dir / filename
|
|
1381
|
+
with open(filepath, 'w') as f:
|
|
1382
|
+
json.dump(pose, f, indent=2)
|
|
1383
|
+
generated_files.append(str(filepath))
|
|
1384
|
+
self.generated_primitives.append(pose)
|
|
1385
|
+
|
|
1386
|
+
return generated_files
|
|
1387
|
+
|
|
1388
|
+
def _generate_quadruped_poses(self) -> List[Dict[str, Any]]:
|
|
1389
|
+
"""Generate poses for quadruped robots."""
|
|
1390
|
+
poses = []
|
|
1391
|
+
|
|
1392
|
+
# Find leg groups
|
|
1393
|
+
leg_groups = [g for g in self.profile.groups.keys() if 'leg' in g.lower()]
|
|
1394
|
+
|
|
1395
|
+
if not leg_groups:
|
|
1396
|
+
return poses
|
|
1397
|
+
|
|
1398
|
+
# stand - All legs at standing position
|
|
1399
|
+
stand_commands = []
|
|
1400
|
+
for actuator in self.profile.actuators:
|
|
1401
|
+
if 'leg' in actuator.group.lower():
|
|
1402
|
+
# For standing: hips at center, upper legs forward, lower legs back
|
|
1403
|
+
if 'hip' in actuator.role:
|
|
1404
|
+
pos = actuator.characteristics.position_center
|
|
1405
|
+
elif 'upper' in actuator.role:
|
|
1406
|
+
pos = actuator.characteristics.position_center + 100
|
|
1407
|
+
elif 'lower' in actuator.role:
|
|
1408
|
+
pos = actuator.characteristics.position_center - 100
|
|
1409
|
+
else:
|
|
1410
|
+
pos = actuator.characteristics.position_center
|
|
1411
|
+
stand_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 800})
|
|
1412
|
+
|
|
1413
|
+
poses.append({
|
|
1414
|
+
"name": "stand",
|
|
1415
|
+
"description": "Move to standing pose with all legs extended",
|
|
1416
|
+
"category": "pose",
|
|
1417
|
+
"commandType": "sequence",
|
|
1418
|
+
"commandTemplate": json.dumps(stand_commands),
|
|
1419
|
+
"parameters": {},
|
|
1420
|
+
"servoIds": [a.id for a in self.profile.actuators if 'leg' in a.group.lower()],
|
|
1421
|
+
"executionTimeMs": 1000,
|
|
1422
|
+
"settleTimeMs": 500
|
|
1423
|
+
})
|
|
1424
|
+
|
|
1425
|
+
# crouch - Low stance
|
|
1426
|
+
crouch_commands = []
|
|
1427
|
+
for actuator in self.profile.actuators:
|
|
1428
|
+
if 'leg' in actuator.group.lower():
|
|
1429
|
+
if 'hip' in actuator.role:
|
|
1430
|
+
pos = actuator.characteristics.position_center
|
|
1431
|
+
elif 'upper' in actuator.role:
|
|
1432
|
+
pos = actuator.characteristics.position_center + 200
|
|
1433
|
+
elif 'lower' in actuator.role:
|
|
1434
|
+
pos = actuator.characteristics.position_center - 200
|
|
1435
|
+
else:
|
|
1436
|
+
pos = actuator.characteristics.position_center
|
|
1437
|
+
crouch_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 600})
|
|
1438
|
+
|
|
1439
|
+
poses.append({
|
|
1440
|
+
"name": "crouch",
|
|
1441
|
+
"description": "Move to crouched/low stance",
|
|
1442
|
+
"category": "pose",
|
|
1443
|
+
"commandType": "sequence",
|
|
1444
|
+
"commandTemplate": json.dumps(crouch_commands),
|
|
1445
|
+
"parameters": {},
|
|
1446
|
+
"servoIds": [a.id for a in self.profile.actuators if 'leg' in a.group.lower()],
|
|
1447
|
+
"executionTimeMs": 800,
|
|
1448
|
+
"settleTimeMs": 300
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
# sit - Sitting pose (back legs tucked)
|
|
1452
|
+
sit_commands = []
|
|
1453
|
+
for actuator in self.profile.actuators:
|
|
1454
|
+
if 'back' in actuator.group.lower() and 'leg' in actuator.group.lower():
|
|
1455
|
+
# Back legs tucked under
|
|
1456
|
+
if 'upper' in actuator.role:
|
|
1457
|
+
pos = actuator.characteristics.position_center + 300
|
|
1458
|
+
elif 'lower' in actuator.role:
|
|
1459
|
+
pos = actuator.characteristics.position_center - 300
|
|
1460
|
+
else:
|
|
1461
|
+
pos = actuator.characteristics.position_center
|
|
1462
|
+
elif 'front' in actuator.group.lower() and 'leg' in actuator.group.lower():
|
|
1463
|
+
# Front legs extended
|
|
1464
|
+
if 'upper' in actuator.role:
|
|
1465
|
+
pos = actuator.characteristics.position_center
|
|
1466
|
+
else:
|
|
1467
|
+
pos = actuator.characteristics.position_center
|
|
1468
|
+
else:
|
|
1469
|
+
continue
|
|
1470
|
+
sit_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 800})
|
|
1471
|
+
|
|
1472
|
+
if sit_commands:
|
|
1473
|
+
poses.append({
|
|
1474
|
+
"name": "sit",
|
|
1475
|
+
"description": "Move to sitting pose with back legs tucked",
|
|
1476
|
+
"category": "pose",
|
|
1477
|
+
"commandType": "sequence",
|
|
1478
|
+
"commandTemplate": json.dumps(sit_commands),
|
|
1479
|
+
"parameters": {},
|
|
1480
|
+
"servoIds": [c["servoId"] for c in sit_commands],
|
|
1481
|
+
"executionTimeMs": 1000,
|
|
1482
|
+
"settleTimeMs": 500
|
|
1483
|
+
})
|
|
1484
|
+
|
|
1485
|
+
return poses
|
|
1486
|
+
|
|
1487
|
+
def _generate_humanoid_poses(self) -> List[Dict[str, Any]]:
|
|
1488
|
+
"""Generate poses for humanoid robots."""
|
|
1489
|
+
poses = []
|
|
1490
|
+
|
|
1491
|
+
# t_pose - Arms out, standing straight
|
|
1492
|
+
t_pose_commands = []
|
|
1493
|
+
for actuator in self.profile.actuators:
|
|
1494
|
+
if 'arm' in actuator.group.lower():
|
|
1495
|
+
# Arms out horizontal
|
|
1496
|
+
if 'shoulder_pitch' in actuator.role:
|
|
1497
|
+
pos = actuator.characteristics.position_center
|
|
1498
|
+
elif 'shoulder_roll' in actuator.role:
|
|
1499
|
+
pos = actuator.characteristics.position_max - 100 # Arms out
|
|
1500
|
+
elif 'elbow' in actuator.role:
|
|
1501
|
+
pos = actuator.characteristics.position_center # Straight
|
|
1502
|
+
else:
|
|
1503
|
+
pos = actuator.characteristics.position_center
|
|
1504
|
+
elif 'leg' in actuator.group.lower():
|
|
1505
|
+
pos = actuator.characteristics.position_center # Standing straight
|
|
1506
|
+
else:
|
|
1507
|
+
pos = actuator.characteristics.position_center
|
|
1508
|
+
t_pose_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 1000})
|
|
1509
|
+
|
|
1510
|
+
poses.append({
|
|
1511
|
+
"name": "t_pose",
|
|
1512
|
+
"description": "T-pose: arms out horizontal, standing straight",
|
|
1513
|
+
"category": "pose",
|
|
1514
|
+
"commandType": "sequence",
|
|
1515
|
+
"commandTemplate": json.dumps(t_pose_commands),
|
|
1516
|
+
"parameters": {},
|
|
1517
|
+
"servoIds": [a.id for a in self.profile.actuators],
|
|
1518
|
+
"executionTimeMs": 1200,
|
|
1519
|
+
"settleTimeMs": 500,
|
|
1520
|
+
"safetyNotes": "Ensure clearance for arm movement"
|
|
1521
|
+
})
|
|
1522
|
+
|
|
1523
|
+
# arms_down - Relaxed standing
|
|
1524
|
+
arms_down_commands = []
|
|
1525
|
+
for actuator in self.profile.actuators:
|
|
1526
|
+
if 'arm' in actuator.group.lower():
|
|
1527
|
+
if 'shoulder_roll' in actuator.role:
|
|
1528
|
+
pos = actuator.characteristics.position_center # Arms at sides
|
|
1529
|
+
elif 'elbow' in actuator.role:
|
|
1530
|
+
pos = actuator.characteristics.position_center + 50 # Slightly bent
|
|
1531
|
+
else:
|
|
1532
|
+
pos = actuator.characteristics.position_center
|
|
1533
|
+
else:
|
|
1534
|
+
pos = actuator.characteristics.position_center
|
|
1535
|
+
arms_down_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 800})
|
|
1536
|
+
|
|
1537
|
+
poses.append({
|
|
1538
|
+
"name": "arms_down",
|
|
1539
|
+
"description": "Relaxed stance with arms at sides",
|
|
1540
|
+
"category": "pose",
|
|
1541
|
+
"commandType": "sequence",
|
|
1542
|
+
"commandTemplate": json.dumps(arms_down_commands),
|
|
1543
|
+
"parameters": {},
|
|
1544
|
+
"servoIds": [a.id for a in self.profile.actuators],
|
|
1545
|
+
"executionTimeMs": 1000,
|
|
1546
|
+
"settleTimeMs": 300
|
|
1547
|
+
})
|
|
1548
|
+
|
|
1549
|
+
# wave_ready - One arm up, ready to wave
|
|
1550
|
+
wave_commands = []
|
|
1551
|
+
for actuator in self.profile.actuators:
|
|
1552
|
+
if 'right_arm' in actuator.group.lower():
|
|
1553
|
+
if 'shoulder_pitch' in actuator.role:
|
|
1554
|
+
pos = actuator.characteristics.position_max - 200 # Arm up
|
|
1555
|
+
elif 'shoulder_roll' in actuator.role:
|
|
1556
|
+
pos = actuator.characteristics.position_center + 100
|
|
1557
|
+
elif 'elbow' in actuator.role:
|
|
1558
|
+
pos = actuator.characteristics.position_center - 100 # Bent
|
|
1559
|
+
else:
|
|
1560
|
+
pos = actuator.characteristics.position_center
|
|
1561
|
+
wave_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 600})
|
|
1562
|
+
|
|
1563
|
+
if wave_commands:
|
|
1564
|
+
poses.append({
|
|
1565
|
+
"name": "wave_ready",
|
|
1566
|
+
"description": "Right arm raised, ready to wave",
|
|
1567
|
+
"category": "pose",
|
|
1568
|
+
"commandType": "sequence",
|
|
1569
|
+
"commandTemplate": json.dumps(wave_commands),
|
|
1570
|
+
"parameters": {},
|
|
1571
|
+
"servoIds": [c["servoId"] for c in wave_commands],
|
|
1572
|
+
"executionTimeMs": 800,
|
|
1573
|
+
"settleTimeMs": 200
|
|
1574
|
+
})
|
|
1575
|
+
|
|
1576
|
+
return poses
|
|
1577
|
+
|
|
1578
|
+
def _generate_arm_poses(self) -> List[Dict[str, Any]]:
|
|
1579
|
+
"""Generate poses for robotic arms."""
|
|
1580
|
+
poses = []
|
|
1581
|
+
|
|
1582
|
+
# home_arm - Safe home position
|
|
1583
|
+
home_commands = [
|
|
1584
|
+
{"servoId": a.id, "position": a.characteristics.position_center, "time_ms": 1000}
|
|
1585
|
+
for a in self.profile.actuators
|
|
1586
|
+
]
|
|
1587
|
+
|
|
1588
|
+
poses.append({
|
|
1589
|
+
"name": "home_arm",
|
|
1590
|
+
"description": "Move arm to safe home position",
|
|
1591
|
+
"category": "pose",
|
|
1592
|
+
"commandType": "sequence",
|
|
1593
|
+
"commandTemplate": json.dumps(home_commands),
|
|
1594
|
+
"parameters": {},
|
|
1595
|
+
"servoIds": [a.id for a in self.profile.actuators],
|
|
1596
|
+
"executionTimeMs": 1200,
|
|
1597
|
+
"settleTimeMs": 500
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
# gripper_open
|
|
1601
|
+
gripper = next((a for a in self.profile.actuators if 'gripper' in a.label.lower()), None)
|
|
1602
|
+
if gripper:
|
|
1603
|
+
poses.append({
|
|
1604
|
+
"name": "gripper_open",
|
|
1605
|
+
"description": "Open the gripper fully",
|
|
1606
|
+
"category": "gripper",
|
|
1607
|
+
"commandType": "single",
|
|
1608
|
+
"commandTemplate": json.dumps({
|
|
1609
|
+
"servoId": gripper.id,
|
|
1610
|
+
"position": gripper.characteristics.position_max,
|
|
1611
|
+
"time_ms": 300
|
|
1612
|
+
}),
|
|
1613
|
+
"parameters": {},
|
|
1614
|
+
"servoIds": [gripper.id],
|
|
1615
|
+
"executionTimeMs": 400,
|
|
1616
|
+
"settleTimeMs": 100
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1619
|
+
poses.append({
|
|
1620
|
+
"name": "gripper_close",
|
|
1621
|
+
"description": "Close the gripper fully",
|
|
1622
|
+
"category": "gripper",
|
|
1623
|
+
"commandType": "single",
|
|
1624
|
+
"commandTemplate": json.dumps({
|
|
1625
|
+
"servoId": gripper.id,
|
|
1626
|
+
"position": gripper.characteristics.position_min,
|
|
1627
|
+
"time_ms": 300
|
|
1628
|
+
}),
|
|
1629
|
+
"parameters": {},
|
|
1630
|
+
"servoIds": [gripper.id],
|
|
1631
|
+
"executionTimeMs": 400,
|
|
1632
|
+
"settleTimeMs": 100
|
|
1633
|
+
})
|
|
1634
|
+
|
|
1635
|
+
# reach_forward - Extend arm forward
|
|
1636
|
+
reach_commands = []
|
|
1637
|
+
for actuator in self.profile.actuators:
|
|
1638
|
+
if 'base' in actuator.role:
|
|
1639
|
+
pos = actuator.characteristics.position_center
|
|
1640
|
+
elif 'shoulder' in actuator.role:
|
|
1641
|
+
pos = actuator.characteristics.position_center + 150
|
|
1642
|
+
elif 'elbow' in actuator.role:
|
|
1643
|
+
pos = actuator.characteristics.position_center - 100
|
|
1644
|
+
elif 'wrist' in actuator.role:
|
|
1645
|
+
pos = actuator.characteristics.position_center
|
|
1646
|
+
else:
|
|
1647
|
+
pos = actuator.characteristics.position_center
|
|
1648
|
+
reach_commands.append({"servoId": actuator.id, "position": pos, "time_ms": 800})
|
|
1649
|
+
|
|
1650
|
+
poses.append({
|
|
1651
|
+
"name": "reach_forward",
|
|
1652
|
+
"description": "Extend arm forward for picking",
|
|
1653
|
+
"category": "pose",
|
|
1654
|
+
"commandType": "sequence",
|
|
1655
|
+
"commandTemplate": json.dumps(reach_commands),
|
|
1656
|
+
"parameters": {},
|
|
1657
|
+
"servoIds": [a.id for a in self.profile.actuators],
|
|
1658
|
+
"executionTimeMs": 1000,
|
|
1659
|
+
"settleTimeMs": 300
|
|
1660
|
+
})
|
|
1661
|
+
|
|
1662
|
+
return poses
|
|
1663
|
+
|
|
1664
|
+
def generate_all(self, output_dir: Path) -> List[str]:
|
|
1665
|
+
"""Generate all files to output directory."""
|
|
1666
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
1667
|
+
|
|
1668
|
+
generated_files = []
|
|
1669
|
+
|
|
1670
|
+
# protocol.json
|
|
1671
|
+
protocol_path = output_dir / "protocol.json"
|
|
1672
|
+
with open(protocol_path, 'w') as f:
|
|
1673
|
+
json.dump(self.generate_protocol_definition(), f, indent=2)
|
|
1674
|
+
generated_files.append(str(protocol_path))
|
|
1675
|
+
|
|
1676
|
+
# servo_map.py
|
|
1677
|
+
servo_map_path = output_dir / "servo_map.py"
|
|
1678
|
+
with open(servo_map_path, 'w') as f:
|
|
1679
|
+
f.write(self.generate_servo_map())
|
|
1680
|
+
generated_files.append(str(servo_map_path))
|
|
1681
|
+
|
|
1682
|
+
# primitives.py
|
|
1683
|
+
primitives_path = output_dir / "primitives.py"
|
|
1684
|
+
with open(primitives_path, 'w') as f:
|
|
1685
|
+
f.write(self.generate_primitives())
|
|
1686
|
+
generated_files.append(str(primitives_path))
|
|
1687
|
+
|
|
1688
|
+
# __init__.py
|
|
1689
|
+
init_path = output_dir / "__init__.py"
|
|
1690
|
+
with open(init_path, 'w') as f:
|
|
1691
|
+
f.write(f'"""Generated robot package for {self.profile.name}."""\n')
|
|
1692
|
+
f.write('from .servo_map import ServoID, SERVO_LIMITS, ALL_SERVOS\n')
|
|
1693
|
+
f.write('from .primitives import PrimitiveSkills, ServoCommand\n')
|
|
1694
|
+
generated_files.append(str(init_path))
|
|
1695
|
+
|
|
1696
|
+
# Generate .primitive.json files for CLI workflow
|
|
1697
|
+
# This is the PostHog-style auto-generation from discovered hardware
|
|
1698
|
+
primitive_json_files = self.generate_primitive_jsons(output_dir)
|
|
1699
|
+
generated_files.extend(primitive_json_files)
|
|
1700
|
+
|
|
1701
|
+
return generated_files
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
class RobotSetupWizard:
|
|
1705
|
+
"""Main wizard orchestrator."""
|
|
1706
|
+
|
|
1707
|
+
TOTAL_STEPS = 5
|
|
1708
|
+
|
|
1709
|
+
def __init__(self, output_dir: str = "./robot"):
|
|
1710
|
+
self.output_dir = Path(output_dir)
|
|
1711
|
+
self.profile = RobotProfile()
|
|
1712
|
+
self.protocol_handler: Optional[ServoProtocolHandler] = None
|
|
1713
|
+
self.ai_assistant = AILabelingAssistant()
|
|
1714
|
+
|
|
1715
|
+
def run(self,
|
|
1716
|
+
port: Optional[str] = None,
|
|
1717
|
+
skip_labeling: bool = False,
|
|
1718
|
+
robot_type: Optional[str] = None,
|
|
1719
|
+
non_interactive: bool = False,
|
|
1720
|
+
push: bool = False,
|
|
1721
|
+
api_url: Optional[str] = None) -> bool:
|
|
1722
|
+
"""Run the complete setup wizard.
|
|
1723
|
+
|
|
1724
|
+
Args:
|
|
1725
|
+
port: Serial port to use (skip device selection)
|
|
1726
|
+
skip_labeling: Skip labeling (use generic names)
|
|
1727
|
+
robot_type: Force robot type (uses AI-suggested labels for that type)
|
|
1728
|
+
non_interactive: Run without user prompts
|
|
1729
|
+
push: Auto-push primitives to FoodforThought (skip confirmation)
|
|
1730
|
+
api_url: FoodforThought API URL (defaults to production)
|
|
1731
|
+
"""
|
|
1732
|
+
|
|
1733
|
+
WizardUI.print_header("Robot Setup Wizard")
|
|
1734
|
+
print("This wizard will help you set up primitive skills for your robot.\n")
|
|
1735
|
+
|
|
1736
|
+
if not non_interactive:
|
|
1737
|
+
print("Requirements:")
|
|
1738
|
+
print(" - Robot connected via USB/serial")
|
|
1739
|
+
print(" - Robot powered on")
|
|
1740
|
+
print(" - Servos in safe position (not bearing load)\n")
|
|
1741
|
+
|
|
1742
|
+
if not WizardUI.confirm("Ready to begin?"):
|
|
1743
|
+
return False
|
|
1744
|
+
|
|
1745
|
+
try:
|
|
1746
|
+
# Step 1: Device Discovery
|
|
1747
|
+
WizardUI.print_step(1, self.TOTAL_STEPS, "Device Discovery")
|
|
1748
|
+
|
|
1749
|
+
if port:
|
|
1750
|
+
device = DiscoveredDevice(port=port, device_type=DeviceType.SERIAL)
|
|
1751
|
+
else:
|
|
1752
|
+
device = self._discover_device(non_interactive=non_interactive)
|
|
1753
|
+
|
|
1754
|
+
if not device:
|
|
1755
|
+
WizardUI.print_error("No device selected. Aborting.")
|
|
1756
|
+
return False
|
|
1757
|
+
|
|
1758
|
+
self.profile.device = device
|
|
1759
|
+
WizardUI.print_success(f"Using device: {device.port}")
|
|
1760
|
+
|
|
1761
|
+
# Step 2: Protocol Detection & Connection
|
|
1762
|
+
WizardUI.print_step(2, self.TOTAL_STEPS, "Protocol Detection")
|
|
1763
|
+
|
|
1764
|
+
if not self._connect_and_detect_protocol(device, non_interactive=non_interactive):
|
|
1765
|
+
return False
|
|
1766
|
+
|
|
1767
|
+
# Step 3: Servo Enumeration
|
|
1768
|
+
WizardUI.print_step(3, self.TOTAL_STEPS, "Servo Enumeration")
|
|
1769
|
+
|
|
1770
|
+
if not self._enumerate_servos():
|
|
1771
|
+
return False
|
|
1772
|
+
|
|
1773
|
+
# Step 4: Component Labeling
|
|
1774
|
+
WizardUI.print_step(4, self.TOTAL_STEPS, "Component Labeling")
|
|
1775
|
+
|
|
1776
|
+
if skip_labeling:
|
|
1777
|
+
# Apply generic labels
|
|
1778
|
+
for actuator in self.profile.actuators:
|
|
1779
|
+
actuator.label = f"servo_{actuator.id}"
|
|
1780
|
+
actuator.group = "main"
|
|
1781
|
+
actuator.role = "actuator"
|
|
1782
|
+
elif robot_type or non_interactive:
|
|
1783
|
+
# Apply AI-suggested labels for specified or detected robot type
|
|
1784
|
+
self._auto_label_components(robot_type)
|
|
1785
|
+
else:
|
|
1786
|
+
# Interactive labeling
|
|
1787
|
+
self._label_components()
|
|
1788
|
+
|
|
1789
|
+
# Step 5: Generate Primitive Skills
|
|
1790
|
+
WizardUI.print_step(5, self.TOTAL_STEPS, "Primitive Skill Generation")
|
|
1791
|
+
|
|
1792
|
+
self._generate_output(auto_push=push, api_url=api_url)
|
|
1793
|
+
|
|
1794
|
+
WizardUI.print_header("Setup Complete!")
|
|
1795
|
+
WizardUI.print_success(f"Generated files in: {self.output_dir}")
|
|
1796
|
+
|
|
1797
|
+
return True
|
|
1798
|
+
|
|
1799
|
+
except KeyboardInterrupt:
|
|
1800
|
+
print("\n")
|
|
1801
|
+
WizardUI.print_warning("Setup cancelled by user.")
|
|
1802
|
+
return False
|
|
1803
|
+
except Exception as e:
|
|
1804
|
+
WizardUI.print_error(f"Setup failed: {e}")
|
|
1805
|
+
import traceback
|
|
1806
|
+
traceback.print_exc()
|
|
1807
|
+
return False
|
|
1808
|
+
finally:
|
|
1809
|
+
if self.protocol_handler:
|
|
1810
|
+
self.protocol_handler.disconnect()
|
|
1811
|
+
|
|
1812
|
+
def _discover_device(self, non_interactive: bool = False) -> Optional[DiscoveredDevice]:
|
|
1813
|
+
"""Discover and select communication device."""
|
|
1814
|
+
if not HAS_SERIAL:
|
|
1815
|
+
WizardUI.print_error("pyserial not installed. Run: pip install pyserial")
|
|
1816
|
+
return None
|
|
1817
|
+
|
|
1818
|
+
WizardUI.print_info("Scanning for serial ports...")
|
|
1819
|
+
|
|
1820
|
+
ports = list(serial.tools.list_ports.comports())
|
|
1821
|
+
|
|
1822
|
+
if not ports:
|
|
1823
|
+
WizardUI.print_warning("No serial ports found.")
|
|
1824
|
+
if non_interactive:
|
|
1825
|
+
return None
|
|
1826
|
+
manual_port = WizardUI.prompt("Enter port manually (or press Enter to abort)")
|
|
1827
|
+
if manual_port:
|
|
1828
|
+
return DiscoveredDevice(port=manual_port, device_type=DeviceType.SERIAL)
|
|
1829
|
+
return None
|
|
1830
|
+
|
|
1831
|
+
# Build device list
|
|
1832
|
+
devices = []
|
|
1833
|
+
for port in ports:
|
|
1834
|
+
device = DiscoveredDevice(
|
|
1835
|
+
port=port.device,
|
|
1836
|
+
device_type=DeviceType.SERIAL,
|
|
1837
|
+
description=port.description,
|
|
1838
|
+
vid=port.vid,
|
|
1839
|
+
pid=port.pid,
|
|
1840
|
+
serial_number=port.serial_number,
|
|
1841
|
+
manufacturer=port.manufacturer
|
|
1842
|
+
)
|
|
1843
|
+
devices.append(device)
|
|
1844
|
+
|
|
1845
|
+
# In non-interactive mode, always auto-detect
|
|
1846
|
+
if non_interactive:
|
|
1847
|
+
return self._auto_detect_robot(devices)
|
|
1848
|
+
|
|
1849
|
+
# Build options list with auto-detect first
|
|
1850
|
+
options = ["🔍 Auto-detect robot (recommended)"]
|
|
1851
|
+
for device in devices:
|
|
1852
|
+
desc = f"{device.port}"
|
|
1853
|
+
if device.description:
|
|
1854
|
+
desc += f" - {device.description}"
|
|
1855
|
+
if device.manufacturer:
|
|
1856
|
+
desc += f" ({device.manufacturer})"
|
|
1857
|
+
options.append(desc)
|
|
1858
|
+
|
|
1859
|
+
selected = WizardUI.select("Select the robot's serial port:", options)
|
|
1860
|
+
if not selected:
|
|
1861
|
+
return None
|
|
1862
|
+
|
|
1863
|
+
selection_idx = selected[0]
|
|
1864
|
+
|
|
1865
|
+
# Handle auto-detect option (index 0)
|
|
1866
|
+
if selection_idx == 0:
|
|
1867
|
+
return self._auto_detect_robot(devices)
|
|
1868
|
+
|
|
1869
|
+
# Otherwise return the selected device (subtract 1 for auto-detect option)
|
|
1870
|
+
return devices[selection_idx - 1]
|
|
1871
|
+
|
|
1872
|
+
def _auto_detect_robot(self, devices: List[DiscoveredDevice]) -> Optional[DiscoveredDevice]:
|
|
1873
|
+
"""Try each port to find one with a robot (servos responding)."""
|
|
1874
|
+
WizardUI.print_info("Auto-detecting robot... (this may take a moment)")
|
|
1875
|
+
|
|
1876
|
+
# Filter out known system ports
|
|
1877
|
+
system_ports = ['debug-console', 'Bluetooth', 'MALS', 'SOC']
|
|
1878
|
+
candidate_devices = [
|
|
1879
|
+
d for d in devices
|
|
1880
|
+
if not any(sp.lower() in d.port.lower() for sp in system_ports)
|
|
1881
|
+
]
|
|
1882
|
+
|
|
1883
|
+
# If no candidates after filtering, try all
|
|
1884
|
+
if not candidate_devices:
|
|
1885
|
+
candidate_devices = devices
|
|
1886
|
+
|
|
1887
|
+
# Try each port
|
|
1888
|
+
for device in candidate_devices:
|
|
1889
|
+
WizardUI.print_info(f" Trying {device.port}...")
|
|
1890
|
+
|
|
1891
|
+
# Try common baud rates
|
|
1892
|
+
for baud in [115200, 1000000, 500000]:
|
|
1893
|
+
try:
|
|
1894
|
+
handler = ServoProtocolHandler(device.port, baud, ProtocolType.HIWONDER)
|
|
1895
|
+
if handler.connect():
|
|
1896
|
+
# Try to ping a few servo IDs
|
|
1897
|
+
for test_id in [1, 0, 2, 3, 5, 10]:
|
|
1898
|
+
if handler.ping(test_id):
|
|
1899
|
+
WizardUI.print_success(f"Found robot on {device.port} at {baud} baud!")
|
|
1900
|
+
device.baud_rate = baud
|
|
1901
|
+
device.detected_protocol = ProtocolType.HIWONDER
|
|
1902
|
+
handler.disconnect()
|
|
1903
|
+
return device
|
|
1904
|
+
handler.disconnect()
|
|
1905
|
+
except Exception:
|
|
1906
|
+
pass
|
|
1907
|
+
|
|
1908
|
+
WizardUI.print_warning("Could not auto-detect robot on any port.")
|
|
1909
|
+
WizardUI.print_info("Please select a port manually or check connections.")
|
|
1910
|
+
|
|
1911
|
+
# Fall back to manual selection (without auto-detect option)
|
|
1912
|
+
options = [f"{d.port} - {d.description or 'n/a'}" for d in devices]
|
|
1913
|
+
selected = WizardUI.select("Select port manually:", options)
|
|
1914
|
+
if selected:
|
|
1915
|
+
return devices[selected[0]]
|
|
1916
|
+
return None
|
|
1917
|
+
|
|
1918
|
+
def _connect_and_detect_protocol(self, device: DiscoveredDevice, non_interactive: bool = False) -> bool:
|
|
1919
|
+
"""Connect to device and detect servo protocol."""
|
|
1920
|
+
|
|
1921
|
+
# Try common baud rates
|
|
1922
|
+
baud_rates = [115200, 1000000, 500000, 57600, 9600]
|
|
1923
|
+
|
|
1924
|
+
WizardUI.print_info(f"Connecting to {device.port}...")
|
|
1925
|
+
|
|
1926
|
+
for baud in baud_rates:
|
|
1927
|
+
WizardUI.print_info(f" Trying {baud} baud...")
|
|
1928
|
+
|
|
1929
|
+
handler = ServoProtocolHandler(device.port, baud, ProtocolType.HIWONDER)
|
|
1930
|
+
|
|
1931
|
+
if handler.connect():
|
|
1932
|
+
# Try to find any servo
|
|
1933
|
+
for test_id in [1, 0, 2, 3]:
|
|
1934
|
+
if handler.ping(test_id):
|
|
1935
|
+
WizardUI.print_success(f"Connected at {baud} baud (HiWonder protocol)")
|
|
1936
|
+
device.baud_rate = baud
|
|
1937
|
+
device.detected_protocol = ProtocolType.HIWONDER
|
|
1938
|
+
self.protocol_handler = handler
|
|
1939
|
+
return True
|
|
1940
|
+
|
|
1941
|
+
handler.disconnect()
|
|
1942
|
+
|
|
1943
|
+
# Fallback: let user specify or use default
|
|
1944
|
+
WizardUI.print_warning("Could not auto-detect protocol.")
|
|
1945
|
+
|
|
1946
|
+
if non_interactive:
|
|
1947
|
+
device.baud_rate = 115200
|
|
1948
|
+
else:
|
|
1949
|
+
baud = WizardUI.prompt("Enter baud rate", "115200")
|
|
1950
|
+
try:
|
|
1951
|
+
device.baud_rate = int(baud)
|
|
1952
|
+
except ValueError:
|
|
1953
|
+
device.baud_rate = 115200
|
|
1954
|
+
|
|
1955
|
+
self.protocol_handler = ServoProtocolHandler(device.port, device.baud_rate)
|
|
1956
|
+
if self.protocol_handler.connect():
|
|
1957
|
+
device.detected_protocol = ProtocolType.HIWONDER
|
|
1958
|
+
return True
|
|
1959
|
+
|
|
1960
|
+
return False
|
|
1961
|
+
|
|
1962
|
+
def _auto_label_components(self, forced_robot_type: Optional[str] = None):
|
|
1963
|
+
"""Apply AI-suggested labels automatically (non-interactive)."""
|
|
1964
|
+
servo_ids = [a.id for a in self.profile.actuators]
|
|
1965
|
+
|
|
1966
|
+
# Get robot type
|
|
1967
|
+
if forced_robot_type:
|
|
1968
|
+
robot_type = forced_robot_type
|
|
1969
|
+
else:
|
|
1970
|
+
robot_type = self.ai_assistant.suggest_robot_type(len(servo_ids), servo_ids)
|
|
1971
|
+
|
|
1972
|
+
# Show detection info
|
|
1973
|
+
core_count = self.ai_assistant._count_core_servos(servo_ids)
|
|
1974
|
+
if core_count < len(servo_ids):
|
|
1975
|
+
WizardUI.print_info(f"Found {len(servo_ids)} servos total, {core_count} appear to be main actuators")
|
|
1976
|
+
WizardUI.print_info(f"Detected robot type: {robot_type}")
|
|
1977
|
+
self.profile.robot_type = robot_type
|
|
1978
|
+
|
|
1979
|
+
# Get suggested labels
|
|
1980
|
+
suggested_labels = self.ai_assistant.suggest_labels(robot_type, servo_ids)
|
|
1981
|
+
|
|
1982
|
+
print("\nApplying suggested labels:")
|
|
1983
|
+
print("-" * 50)
|
|
1984
|
+
|
|
1985
|
+
for actuator in self.profile.actuators:
|
|
1986
|
+
if actuator.id in suggested_labels:
|
|
1987
|
+
actuator.label = suggested_labels[actuator.id]['label']
|
|
1988
|
+
actuator.group = suggested_labels[actuator.id]['group']
|
|
1989
|
+
actuator.role = suggested_labels[actuator.id]['role']
|
|
1990
|
+
print(f" Servo {actuator.id}: {actuator.label} ({actuator.group}/{actuator.role})")
|
|
1991
|
+
else:
|
|
1992
|
+
actuator.label = f"servo_{actuator.id}"
|
|
1993
|
+
actuator.group = "extra"
|
|
1994
|
+
actuator.role = "unknown"
|
|
1995
|
+
print(f" Servo {actuator.id}: {actuator.label} (extra/unknown)")
|
|
1996
|
+
|
|
1997
|
+
# Build groups
|
|
1998
|
+
self.profile.groups = {}
|
|
1999
|
+
for actuator in self.profile.actuators:
|
|
2000
|
+
if actuator.group not in self.profile.groups:
|
|
2001
|
+
self.profile.groups[actuator.group] = []
|
|
2002
|
+
self.profile.groups[actuator.group].append(actuator.id)
|
|
2003
|
+
|
|
2004
|
+
def _enumerate_servos(self) -> bool:
|
|
2005
|
+
"""Scan for and enumerate all servos."""
|
|
2006
|
+
|
|
2007
|
+
WizardUI.print_info("Scanning for servos (this may take a moment)...")
|
|
2008
|
+
|
|
2009
|
+
id_range = range(0, 32) # Typical range for small robots
|
|
2010
|
+
|
|
2011
|
+
def progress(current, total):
|
|
2012
|
+
bar_len = 30
|
|
2013
|
+
filled = int(bar_len * current / total)
|
|
2014
|
+
bar = '█' * filled + '░' * (bar_len - filled)
|
|
2015
|
+
print(f"\r Scanning: [{bar}] {current}/{total}", end='', flush=True)
|
|
2016
|
+
|
|
2017
|
+
found_ids = self.protocol_handler.scan_servos(id_range, progress)
|
|
2018
|
+
print() # New line after progress bar
|
|
2019
|
+
|
|
2020
|
+
if not found_ids:
|
|
2021
|
+
WizardUI.print_warning("No servos found in ID range 0-31.")
|
|
2022
|
+
|
|
2023
|
+
if WizardUI.confirm("Scan extended range (0-253)?"):
|
|
2024
|
+
found_ids = self.protocol_handler.scan_servos(range(0, 254), progress)
|
|
2025
|
+
print()
|
|
2026
|
+
|
|
2027
|
+
if not found_ids:
|
|
2028
|
+
WizardUI.print_error("No servos found. Check connections and power.")
|
|
2029
|
+
return False
|
|
2030
|
+
|
|
2031
|
+
WizardUI.print_success(f"Found {len(found_ids)} servos: {found_ids}")
|
|
2032
|
+
|
|
2033
|
+
# Create actuator entries
|
|
2034
|
+
for servo_id in found_ids:
|
|
2035
|
+
chars = self.protocol_handler.characterize_servo(servo_id)
|
|
2036
|
+
|
|
2037
|
+
actuator = DiscoveredActuator(
|
|
2038
|
+
id=servo_id,
|
|
2039
|
+
actuator_type=ActuatorType.SERIAL_BUS_SERVO,
|
|
2040
|
+
protocol=self.profile.device.detected_protocol or ProtocolType.HIWONDER,
|
|
2041
|
+
characteristics=chars,
|
|
2042
|
+
verified_working=True
|
|
2043
|
+
)
|
|
2044
|
+
self.profile.actuators.append(actuator)
|
|
2045
|
+
|
|
2046
|
+
return True
|
|
2047
|
+
|
|
2048
|
+
def _label_components(self):
|
|
2049
|
+
"""Run interactive labeling session."""
|
|
2050
|
+
|
|
2051
|
+
# Get robot name
|
|
2052
|
+
self.profile.name = WizardUI.prompt("Robot name", "my_robot")
|
|
2053
|
+
self.profile.manufacturer = WizardUI.prompt("Manufacturer (optional)", "")
|
|
2054
|
+
self.profile.model = WizardUI.prompt("Model (optional)", "")
|
|
2055
|
+
|
|
2056
|
+
# AI-assisted labeling
|
|
2057
|
+
self.ai_assistant.interactive_label_session(self.profile, self.protocol_handler)
|
|
2058
|
+
|
|
2059
|
+
def _generate_output(self, auto_push: bool = False, api_url: Optional[str] = None):
|
|
2060
|
+
"""Generate all output files and optionally push to platform.
|
|
2061
|
+
|
|
2062
|
+
Args:
|
|
2063
|
+
auto_push: If True, push primitives to FoodforThought without prompting
|
|
2064
|
+
api_url: FoodforThought API URL (defaults to production)
|
|
2065
|
+
"""
|
|
2066
|
+
generator = PrimitiveSkillGenerator(self.profile)
|
|
2067
|
+
generated_files = generator.generate_all(self.output_dir)
|
|
2068
|
+
|
|
2069
|
+
# Separate by type
|
|
2070
|
+
json_files = [f for f in generated_files if f.endswith('.primitive.json')]
|
|
2071
|
+
other_files = [f for f in generated_files if not f.endswith('.primitive.json')]
|
|
2072
|
+
|
|
2073
|
+
print("\nGenerated files:")
|
|
2074
|
+
for f in other_files:
|
|
2075
|
+
print(f" - {f}")
|
|
2076
|
+
|
|
2077
|
+
if json_files:
|
|
2078
|
+
print(f"\n{WizardUI.COLORS['cyan']}Generated {len(json_files)} primitive skills:{WizardUI.COLORS['reset']}")
|
|
2079
|
+
primitives_dir = self.output_dir / "primitives"
|
|
2080
|
+
print(f" - {primitives_dir}/*.primitive.json")
|
|
2081
|
+
|
|
2082
|
+
# Show breakdown by category
|
|
2083
|
+
categories = {}
|
|
2084
|
+
for p in generator.generated_primitives:
|
|
2085
|
+
cat = p.get('category', 'other')
|
|
2086
|
+
categories[cat] = categories.get(cat, 0) + 1
|
|
2087
|
+
print("\n Breakdown:")
|
|
2088
|
+
for cat, count in sorted(categories.items()):
|
|
2089
|
+
print(f" - {cat}: {count}")
|
|
2090
|
+
|
|
2091
|
+
# Push flow
|
|
2092
|
+
self.generator = generator # Store for access to primitives
|
|
2093
|
+
|
|
2094
|
+
if generator.generated_primitives:
|
|
2095
|
+
if auto_push:
|
|
2096
|
+
self._push_primitives(api_url)
|
|
2097
|
+
else:
|
|
2098
|
+
print(f"\n{WizardUI.COLORS['bold']}Ready to share with the community?{WizardUI.COLORS['reset']}")
|
|
2099
|
+
if WizardUI.confirm(f"Push {len(generator.generated_primitives)} primitives to FoodforThought?"):
|
|
2100
|
+
self._push_primitives(api_url)
|
|
2101
|
+
else:
|
|
2102
|
+
print("\nYou can push later with:")
|
|
2103
|
+
print(f" ate primitive push {self.output_dir / 'primitives'}/*.primitive.json")
|
|
2104
|
+
|
|
2105
|
+
def _push_primitives(self, api_url: Optional[str] = None):
|
|
2106
|
+
"""Push generated primitives to FoodforThought platform."""
|
|
2107
|
+
import requests
|
|
2108
|
+
|
|
2109
|
+
base_url = api_url or os.getenv("FOODFORTHOUGHT_API_URL", "https://kindlyrobotics.com")
|
|
2110
|
+
api_endpoint = f"{base_url}/api/primitives"
|
|
2111
|
+
|
|
2112
|
+
# Check for auth token
|
|
2113
|
+
token = os.getenv("FOODFORTHOUGHT_TOKEN") or os.getenv("ATE_TOKEN")
|
|
2114
|
+
if not token:
|
|
2115
|
+
WizardUI.print_warning("No API token found. Set FOODFORTHOUGHT_TOKEN or ATE_TOKEN")
|
|
2116
|
+
WizardUI.print_info("Get your token at: https://kindlyrobotics.com/foodforthought/settings")
|
|
2117
|
+
return
|
|
2118
|
+
|
|
2119
|
+
headers = {
|
|
2120
|
+
"Authorization": f"Bearer {token}",
|
|
2121
|
+
"Content-Type": "application/json"
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
print(f"\n{WizardUI.COLORS['cyan']}Pushing primitives to FoodforThought...{WizardUI.COLORS['reset']}")
|
|
2125
|
+
|
|
2126
|
+
success_count = 0
|
|
2127
|
+
fail_count = 0
|
|
2128
|
+
|
|
2129
|
+
for primitive in self.generator.generated_primitives:
|
|
2130
|
+
try:
|
|
2131
|
+
# Transform to API format
|
|
2132
|
+
payload = {
|
|
2133
|
+
"name": primitive["name"],
|
|
2134
|
+
"description": primitive.get("description", ""),
|
|
2135
|
+
"category": primitive.get("category", "motion"),
|
|
2136
|
+
"commandTemplate": primitive.get("commandTemplate", ""),
|
|
2137
|
+
"commandType": primitive.get("commandType", "single"),
|
|
2138
|
+
"parameters": primitive.get("parameters", {}),
|
|
2139
|
+
"executionTimeMs": primitive.get("executionTimeMs"),
|
|
2140
|
+
"settleTimeMs": primitive.get("settleTimeMs"),
|
|
2141
|
+
"cooldownMs": primitive.get("cooldownMs"),
|
|
2142
|
+
"safetyNotes": primitive.get("safetyNotes"),
|
|
2143
|
+
"robotModel": primitive.get("robotModel"),
|
|
2144
|
+
"robotType": primitive.get("robotType"),
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
# Add protocol reference if available
|
|
2148
|
+
if primitive.get("protocol"):
|
|
2149
|
+
payload["protocol"] = primitive["protocol"]
|
|
2150
|
+
|
|
2151
|
+
response = requests.post(api_endpoint, json=payload, headers=headers, timeout=10)
|
|
2152
|
+
|
|
2153
|
+
if response.status_code in (200, 201):
|
|
2154
|
+
success_count += 1
|
|
2155
|
+
else:
|
|
2156
|
+
fail_count += 1
|
|
2157
|
+
if fail_count == 1:
|
|
2158
|
+
# Show first error
|
|
2159
|
+
WizardUI.print_warning(f"Failed to push {primitive['name']}: {response.text[:100]}")
|
|
2160
|
+
|
|
2161
|
+
except requests.exceptions.RequestException as e:
|
|
2162
|
+
fail_count += 1
|
|
2163
|
+
if fail_count == 1:
|
|
2164
|
+
WizardUI.print_error(f"Network error: {e}")
|
|
2165
|
+
if "kindlyrobotics.com" in str(api_endpoint):
|
|
2166
|
+
WizardUI.print_info("Make sure you're connected to the internet")
|
|
2167
|
+
|
|
2168
|
+
# Summary
|
|
2169
|
+
if success_count > 0:
|
|
2170
|
+
WizardUI.print_success(f"Pushed {success_count} primitives to FoodforThought!")
|
|
2171
|
+
print(f" View at: {base_url}/foodforthought/robots")
|
|
2172
|
+
if fail_count > 0:
|
|
2173
|
+
WizardUI.print_warning(f"{fail_count} primitives failed to push")
|
|
2174
|
+
print(f" Retry with: ate primitive push {self.output_dir / 'primitives'}/*.primitive.json")
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
def run_wizard(port: Optional[str] = None,
|
|
2178
|
+
output: str = "./robot",
|
|
2179
|
+
skip_labeling: bool = False,
|
|
2180
|
+
robot_type: Optional[str] = None,
|
|
2181
|
+
non_interactive: bool = False,
|
|
2182
|
+
push: bool = False,
|
|
2183
|
+
api_url: Optional[str] = None) -> bool:
|
|
2184
|
+
"""Entry point for CLI.
|
|
2185
|
+
|
|
2186
|
+
Args:
|
|
2187
|
+
port: Serial port to use
|
|
2188
|
+
output: Output directory for generated files
|
|
2189
|
+
skip_labeling: Skip labeling entirely (generic names)
|
|
2190
|
+
robot_type: Force robot type (e.g., 'quadruped', 'humanoid_full')
|
|
2191
|
+
non_interactive: Run without user prompts (use defaults)
|
|
2192
|
+
push: Auto-push primitives to FoodforThought platform
|
|
2193
|
+
api_url: FoodforThought API URL (defaults to production)
|
|
2194
|
+
"""
|
|
2195
|
+
wizard = RobotSetupWizard(output_dir=output)
|
|
2196
|
+
return wizard.run(
|
|
2197
|
+
port=port,
|
|
2198
|
+
skip_labeling=skip_labeling,
|
|
2199
|
+
robot_type=robot_type,
|
|
2200
|
+
non_interactive=non_interactive,
|
|
2201
|
+
push=push,
|
|
2202
|
+
api_url=api_url
|
|
2203
|
+
)
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
if __name__ == "__main__":
|
|
2207
|
+
# Quick test
|
|
2208
|
+
import argparse
|
|
2209
|
+
parser = argparse.ArgumentParser(description="Robot Setup Wizard")
|
|
2210
|
+
parser.add_argument("--port", "-p", help="Serial port (skip device selection)")
|
|
2211
|
+
parser.add_argument("--output", "-o", default="./robot", help="Output directory")
|
|
2212
|
+
parser.add_argument("--skip-labeling", action="store_true", help="Skip interactive labeling")
|
|
2213
|
+
|
|
2214
|
+
args = parser.parse_args()
|
|
2215
|
+
|
|
2216
|
+
success = run_wizard(
|
|
2217
|
+
port=args.port,
|
|
2218
|
+
output=args.output,
|
|
2219
|
+
skip_labeling=args.skip_labeling
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2222
|
+
sys.exit(0 if success else 1)
|