foodforthought-cli 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +100 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/behaviors/common.py +686 -0
  9. ate/behaviors/tree.py +454 -0
  10. ate/cli.py +855 -3995
  11. ate/client.py +90 -0
  12. ate/commands/__init__.py +168 -0
  13. ate/commands/auth.py +389 -0
  14. ate/commands/bridge.py +448 -0
  15. ate/commands/data.py +185 -0
  16. ate/commands/deps.py +111 -0
  17. ate/commands/generate.py +384 -0
  18. ate/commands/memory.py +907 -0
  19. ate/commands/parts.py +166 -0
  20. ate/commands/primitive.py +399 -0
  21. ate/commands/protocol.py +288 -0
  22. ate/commands/recording.py +524 -0
  23. ate/commands/repo.py +154 -0
  24. ate/commands/simulation.py +291 -0
  25. ate/commands/skill.py +303 -0
  26. ate/commands/skills.py +487 -0
  27. ate/commands/team.py +147 -0
  28. ate/commands/workflow.py +271 -0
  29. ate/detection/__init__.py +38 -0
  30. ate/detection/base.py +142 -0
  31. ate/detection/color_detector.py +399 -0
  32. ate/detection/trash_detector.py +322 -0
  33. ate/drivers/__init__.py +39 -0
  34. ate/drivers/ble_transport.py +405 -0
  35. ate/drivers/mechdog.py +942 -0
  36. ate/drivers/wifi_camera.py +477 -0
  37. ate/interfaces/__init__.py +187 -0
  38. ate/interfaces/base.py +273 -0
  39. ate/interfaces/body.py +267 -0
  40. ate/interfaces/detection.py +282 -0
  41. ate/interfaces/locomotion.py +422 -0
  42. ate/interfaces/manipulation.py +408 -0
  43. ate/interfaces/navigation.py +389 -0
  44. ate/interfaces/perception.py +362 -0
  45. ate/interfaces/sensors.py +247 -0
  46. ate/interfaces/types.py +371 -0
  47. ate/llm_proxy.py +239 -0
  48. ate/mcp_server.py +387 -0
  49. ate/memory/__init__.py +35 -0
  50. ate/memory/cloud.py +244 -0
  51. ate/memory/context.py +269 -0
  52. ate/memory/embeddings.py +184 -0
  53. ate/memory/export.py +26 -0
  54. ate/memory/merge.py +146 -0
  55. ate/memory/migrate/__init__.py +34 -0
  56. ate/memory/migrate/base.py +89 -0
  57. ate/memory/migrate/pipeline.py +189 -0
  58. ate/memory/migrate/sources/__init__.py +13 -0
  59. ate/memory/migrate/sources/chroma.py +170 -0
  60. ate/memory/migrate/sources/pinecone.py +120 -0
  61. ate/memory/migrate/sources/qdrant.py +110 -0
  62. ate/memory/migrate/sources/weaviate.py +160 -0
  63. ate/memory/reranker.py +353 -0
  64. ate/memory/search.py +26 -0
  65. ate/memory/store.py +548 -0
  66. ate/recording/__init__.py +83 -0
  67. ate/recording/demonstration.py +378 -0
  68. ate/recording/session.py +415 -0
  69. ate/recording/upload.py +304 -0
  70. ate/recording/visual.py +416 -0
  71. ate/recording/wrapper.py +95 -0
  72. ate/robot/__init__.py +221 -0
  73. ate/robot/agentic_servo.py +856 -0
  74. ate/robot/behaviors.py +493 -0
  75. ate/robot/ble_capture.py +1000 -0
  76. ate/robot/ble_enumerate.py +506 -0
  77. ate/robot/calibration.py +668 -0
  78. ate/robot/calibration_state.py +388 -0
  79. ate/robot/commands.py +3735 -0
  80. ate/robot/direction_calibration.py +554 -0
  81. ate/robot/discovery.py +441 -0
  82. ate/robot/introspection.py +330 -0
  83. ate/robot/llm_system_id.py +654 -0
  84. ate/robot/locomotion_calibration.py +508 -0
  85. ate/robot/manager.py +270 -0
  86. ate/robot/marker_generator.py +611 -0
  87. ate/robot/perception.py +502 -0
  88. ate/robot/primitives.py +614 -0
  89. ate/robot/profiles.py +281 -0
  90. ate/robot/registry.py +322 -0
  91. ate/robot/servo_mapper.py +1153 -0
  92. ate/robot/skill_upload.py +675 -0
  93. ate/robot/target_calibration.py +500 -0
  94. ate/robot/teach.py +515 -0
  95. ate/robot/types.py +242 -0
  96. ate/robot/visual_labeler.py +1048 -0
  97. ate/robot/visual_servo_loop.py +494 -0
  98. ate/robot/visual_servoing.py +570 -0
  99. ate/robot/visual_system_id.py +906 -0
  100. ate/transports/__init__.py +121 -0
  101. ate/transports/base.py +394 -0
  102. ate/transports/ble.py +405 -0
  103. ate/transports/hybrid.py +444 -0
  104. ate/transports/serial.py +345 -0
  105. ate/urdf/__init__.py +30 -0
  106. ate/urdf/capture.py +582 -0
  107. ate/urdf/cloud.py +491 -0
  108. ate/urdf/collision.py +271 -0
  109. ate/urdf/commands.py +708 -0
  110. ate/urdf/depth.py +360 -0
  111. ate/urdf/inertial.py +312 -0
  112. ate/urdf/kinematics.py +330 -0
  113. ate/urdf/lifting.py +415 -0
  114. ate/urdf/meshing.py +300 -0
  115. ate/urdf/models/__init__.py +110 -0
  116. ate/urdf/models/depth_anything.py +253 -0
  117. ate/urdf/models/sam2.py +324 -0
  118. ate/urdf/motion_analysis.py +396 -0
  119. ate/urdf/pipeline.py +468 -0
  120. ate/urdf/scale.py +256 -0
  121. ate/urdf/scan_session.py +411 -0
  122. ate/urdf/segmentation.py +299 -0
  123. ate/urdf/synthesis.py +319 -0
  124. ate/urdf/topology.py +336 -0
  125. ate/urdf/validation.py +371 -0
  126. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
  127. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  128. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  129. foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
  130. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  131. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ArUco Marker PDF Generator
4
+ ===========================
5
+
6
+ Generates printable ArUco markers for robot calibration.
7
+ Solves the pain point of users needing to figure out marker sizes,
8
+ print settings, and cutting guides.
9
+
10
+ Usage:
11
+ ate robot generate-markers --output markers.pdf
12
+ ate robot generate-markers --count 10 --size 30 --output markers.pdf
13
+ """
14
+
15
+ import io
16
+ import math
17
+ from pathlib import Path
18
+ from typing import Optional, List, Tuple
19
+ from dataclasses import dataclass
20
+
21
+ try:
22
+ import cv2
23
+ import numpy as np
24
+ HAS_OPENCV = True
25
+ except ImportError:
26
+ HAS_OPENCV = False
27
+
28
+ try:
29
+ from reportlab.lib.pagesizes import LETTER, A4
30
+ from reportlab.lib.units import mm, inch
31
+ from reportlab.pdfgen import canvas
32
+ from reportlab.lib.colors import black, gray, white, lightgrey
33
+ HAS_REPORTLAB = True
34
+ except ImportError:
35
+ HAS_REPORTLAB = False
36
+
37
+
38
+ @dataclass
39
+ class MarkerSpec:
40
+ """Specification for a single marker."""
41
+ id: int
42
+ label: str
43
+ size_mm: float
44
+
45
+
46
+ def check_dependencies():
47
+ """Check if required dependencies are installed."""
48
+ missing = []
49
+ if not HAS_OPENCV:
50
+ missing.append("opencv-contrib-python")
51
+ if not HAS_REPORTLAB:
52
+ missing.append("reportlab")
53
+
54
+ if missing:
55
+ raise ImportError(
56
+ f"Missing required packages: {', '.join(missing)}. "
57
+ f"Install with: pip install {' '.join(missing)}"
58
+ )
59
+
60
+
61
+ def generate_aruco_image(
62
+ marker_id: int,
63
+ size_pixels: int = 200,
64
+ dictionary_id: int = cv2.aruco.DICT_4X4_50
65
+ ) -> np.ndarray:
66
+ """Generate an ArUco marker image.
67
+
68
+ Args:
69
+ marker_id: The marker ID (0-49 for DICT_4X4_50)
70
+ size_pixels: Output image size in pixels
71
+ dictionary_id: ArUco dictionary to use
72
+
73
+ Returns:
74
+ Numpy array with the marker image (grayscale)
75
+ """
76
+ dictionary = cv2.aruco.getPredefinedDictionary(dictionary_id)
77
+ marker_image = cv2.aruco.generateImageMarker(dictionary, marker_id, size_pixels)
78
+ return marker_image
79
+
80
+
81
+ def generate_marker_pdf(
82
+ output_path: str,
83
+ marker_specs: Optional[List[MarkerSpec]] = None,
84
+ count: int = 12,
85
+ size_mm: float = 30.0,
86
+ page_size: Tuple[float, float] = LETTER,
87
+ include_instructions: bool = True,
88
+ robot_name: Optional[str] = None,
89
+ ) -> str:
90
+ """Generate a PDF with printable ArUco markers.
91
+
92
+ Args:
93
+ output_path: Path to save the PDF
94
+ marker_specs: Optional list of marker specifications. If None, generates
95
+ generic markers with IDs 0 to count-1.
96
+ count: Number of markers to generate (ignored if marker_specs provided)
97
+ size_mm: Marker size in millimeters (ignored if marker_specs provided)
98
+ page_size: Page size tuple (width, height) in points
99
+ include_instructions: Include usage instructions on first page
100
+ robot_name: Optional robot name for labeling
101
+
102
+ Returns:
103
+ Path to the generated PDF
104
+ """
105
+ check_dependencies()
106
+
107
+ # Default marker specs if not provided
108
+ if marker_specs is None:
109
+ marker_specs = [
110
+ MarkerSpec(id=i, label=f"Joint {i}", size_mm=size_mm)
111
+ for i in range(count)
112
+ ]
113
+
114
+ # Create PDF
115
+ c = canvas.Canvas(output_path, pagesize=page_size)
116
+ page_width, page_height = page_size
117
+
118
+ # Margins
119
+ margin = 0.5 * inch
120
+ usable_width = page_width - 2 * margin
121
+ usable_height = page_height - 2 * margin
122
+
123
+ # Instructions page
124
+ if include_instructions:
125
+ _draw_instructions_page(c, page_width, page_height, robot_name)
126
+ c.showPage()
127
+
128
+ # Calculate grid layout
129
+ # Add 10mm margin around each marker for cutting
130
+ marker_with_margin = size_mm + 20 # 10mm on each side
131
+
132
+ cols = int(usable_width / (marker_with_margin * mm))
133
+ rows = int(usable_height / (marker_with_margin * mm))
134
+
135
+ if cols < 1:
136
+ cols = 1
137
+ if rows < 1:
138
+ rows = 1
139
+
140
+ markers_per_page = cols * rows
141
+
142
+ # Center the grid
143
+ grid_width = cols * marker_with_margin * mm
144
+ grid_height = rows * marker_with_margin * mm
145
+ start_x = (page_width - grid_width) / 2 + 10 * mm
146
+ start_y = page_height - (page_height - grid_height) / 2 - 10 * mm - size_mm * mm
147
+
148
+ current_marker = 0
149
+
150
+ while current_marker < len(marker_specs):
151
+ # Draw markers on this page
152
+ for row in range(rows):
153
+ if current_marker >= len(marker_specs):
154
+ break
155
+ for col in range(cols):
156
+ if current_marker >= len(marker_specs):
157
+ break
158
+
159
+ spec = marker_specs[current_marker]
160
+ x = start_x + col * marker_with_margin * mm
161
+ y = start_y - row * marker_with_margin * mm
162
+
163
+ _draw_marker_cell(c, spec, x, y, spec.size_mm * mm)
164
+ current_marker += 1
165
+
166
+ # Page number
167
+ c.setFont("Helvetica", 9)
168
+ c.setFillColor(gray)
169
+ page_num = (current_marker - 1) // markers_per_page + 1
170
+ if include_instructions:
171
+ page_num += 1
172
+ c.drawCentredString(page_width / 2, 0.3 * inch, f"Page {page_num}")
173
+
174
+ if current_marker < len(marker_specs):
175
+ c.showPage()
176
+
177
+ c.save()
178
+ return output_path
179
+
180
+
181
+ def _draw_instructions_page(
182
+ c: canvas.Canvas,
183
+ page_width: float,
184
+ page_height: float,
185
+ robot_name: Optional[str] = None,
186
+ ):
187
+ """Draw the instructions page."""
188
+ # Title
189
+ c.setFont("Helvetica-Bold", 24)
190
+ c.setFillColor(black)
191
+ title = "ArUco Calibration Markers"
192
+ if robot_name:
193
+ title = f"ArUco Markers for {robot_name}"
194
+ c.drawCentredString(page_width / 2, page_height - 1 * inch, title)
195
+
196
+ # Subtitle
197
+ c.setFont("Helvetica", 14)
198
+ c.setFillColor(gray)
199
+ c.drawCentredString(
200
+ page_width / 2,
201
+ page_height - 1.4 * inch,
202
+ "FoodforThought Robot Calibration System"
203
+ )
204
+
205
+ # Instructions box
206
+ box_x = 1 * inch
207
+ box_y = page_height - 6 * inch
208
+ box_width = page_width - 2 * inch
209
+ box_height = 4 * inch
210
+
211
+ c.setFillColor(lightgrey)
212
+ c.rect(box_x, box_y, box_width, box_height, fill=True, stroke=False)
213
+
214
+ c.setFillColor(black)
215
+ c.setFont("Helvetica-Bold", 14)
216
+ c.drawString(box_x + 0.2 * inch, box_y + box_height - 0.4 * inch, "Instructions")
217
+
218
+ instructions = [
219
+ "1. Print this PDF at 100% scale (do not fit to page)",
220
+ "2. Cut out each marker along the dashed lines",
221
+ "3. Attach markers to each moving joint of your robot:",
222
+ " - Use tape or adhesive putty for temporary mounting",
223
+ " - Place marker on rigid part of each joint segment",
224
+ " - Ensure markers face the camera and are visible",
225
+ "4. Run the calibration command:",
226
+ " ate robot calibrate --method aruco",
227
+ "5. Follow the on-screen prompts to wiggle each joint",
228
+ "6. Upload your calibration to share with the community!",
229
+ ]
230
+
231
+ c.setFont("Helvetica", 11)
232
+ y = box_y + box_height - 0.8 * inch
233
+ for line in instructions:
234
+ c.drawString(box_x + 0.3 * inch, y, line)
235
+ y -= 0.35 * inch
236
+
237
+ # Tips box
238
+ tips_y = box_y - 0.5 * inch - 2.5 * inch
239
+ tips_height = 2.5 * inch
240
+
241
+ c.setFillColor(white)
242
+ c.setStrokeColor(gray)
243
+ c.rect(box_x, tips_y, box_width, tips_height, fill=True, stroke=True)
244
+
245
+ c.setFillColor(black)
246
+ c.setFont("Helvetica-Bold", 12)
247
+ c.drawString(box_x + 0.2 * inch, tips_y + tips_height - 0.4 * inch, "Tips for Best Results")
248
+
249
+ tips = [
250
+ "- Ensure good, even lighting (avoid shadows and glare)",
251
+ "- Keep markers flat and perpendicular to camera when possible",
252
+ "- Use the largest markers that fit on your robot's joints",
253
+ "- Each marker ID should only appear once on the robot",
254
+ "- Test detection before running full calibration",
255
+ ]
256
+
257
+ c.setFont("Helvetica", 10)
258
+ y = tips_y + tips_height - 0.7 * inch
259
+ for tip in tips:
260
+ c.drawString(box_x + 0.3 * inch, y, tip)
261
+ y -= 0.3 * inch
262
+
263
+ # Footer
264
+ c.setFont("Helvetica", 9)
265
+ c.setFillColor(gray)
266
+ c.drawCentredString(
267
+ page_width / 2,
268
+ 0.5 * inch,
269
+ "Generated by FoodforThought CLI | kindlyrobotics.com"
270
+ )
271
+
272
+
273
+ def _draw_marker_cell(
274
+ c: canvas.Canvas,
275
+ spec: MarkerSpec,
276
+ x: float,
277
+ y: float,
278
+ size: float,
279
+ ):
280
+ """Draw a single marker with cutting guides and label."""
281
+ # Generate marker image
282
+ marker_img = generate_aruco_image(spec.id, size_pixels=300)
283
+
284
+ # Convert to PIL for ReportLab
285
+ from PIL import Image
286
+ pil_img = Image.fromarray(marker_img)
287
+
288
+ # Save to bytes
289
+ img_buffer = io.BytesIO()
290
+ pil_img.save(img_buffer, format='PNG')
291
+ img_buffer.seek(0)
292
+
293
+ # Draw cutting guides (dashed rectangle)
294
+ padding = 5 * mm
295
+ cut_x = x - padding
296
+ cut_y = y - padding
297
+ cut_size = size + 2 * padding
298
+
299
+ c.setStrokeColor(gray)
300
+ c.setDash(3, 3)
301
+ c.rect(cut_x, cut_y, cut_size, cut_size, fill=False, stroke=True)
302
+ c.setDash() # Reset to solid
303
+
304
+ # Corner marks for cutting
305
+ mark_len = 3 * mm
306
+ c.setStrokeColor(black)
307
+ c.setLineWidth(0.5)
308
+
309
+ # Top-left corner
310
+ c.line(cut_x - mark_len, cut_y + cut_size, cut_x, cut_y + cut_size)
311
+ c.line(cut_x, cut_y + cut_size, cut_x, cut_y + cut_size + mark_len)
312
+
313
+ # Top-right corner
314
+ c.line(cut_x + cut_size, cut_y + cut_size, cut_x + cut_size + mark_len, cut_y + cut_size)
315
+ c.line(cut_x + cut_size, cut_y + cut_size, cut_x + cut_size, cut_y + cut_size + mark_len)
316
+
317
+ # Bottom-left corner
318
+ c.line(cut_x - mark_len, cut_y, cut_x, cut_y)
319
+ c.line(cut_x, cut_y, cut_x, cut_y - mark_len)
320
+
321
+ # Bottom-right corner
322
+ c.line(cut_x + cut_size, cut_y, cut_x + cut_size + mark_len, cut_y)
323
+ c.line(cut_x + cut_size, cut_y, cut_x + cut_size, cut_y - mark_len)
324
+
325
+ # Draw the marker image
326
+ from reportlab.lib.utils import ImageReader
327
+ img_reader = ImageReader(img_buffer)
328
+ c.drawImage(img_reader, x, y, width=size, height=size)
329
+
330
+ # Label below marker
331
+ c.setFont("Helvetica", 8)
332
+ c.setFillColor(black)
333
+ label = f"ID: {spec.id}"
334
+ if spec.label:
335
+ label = f"{spec.label} (ID: {spec.id})"
336
+ c.drawCentredString(x + size / 2, y - 4 * mm, label)
337
+
338
+ # Size indicator
339
+ c.setFont("Helvetica", 6)
340
+ c.setFillColor(gray)
341
+ c.drawCentredString(x + size / 2, y - 7 * mm, f"{spec.size_mm:.0f}mm")
342
+
343
+
344
+ def generate_robot_markers(
345
+ robot_joints: List[dict],
346
+ output_path: str,
347
+ robot_name: str,
348
+ base_size_mm: float = 25.0,
349
+ ) -> str:
350
+ """Generate markers specifically for a robot's joints.
351
+
352
+ Args:
353
+ robot_joints: List of joint dicts with 'name', 'type', optional 'segment'
354
+ output_path: Path to save the PDF
355
+ robot_name: Name of the robot
356
+ base_size_mm: Base marker size in mm
357
+
358
+ Returns:
359
+ Path to the generated PDF
360
+ """
361
+ # Create marker specs from joint definitions
362
+ specs = []
363
+ for i, joint in enumerate(robot_joints):
364
+ name = joint.get('name', f'Joint {i}')
365
+ segment = joint.get('segment', '')
366
+
367
+ label = name
368
+ if segment:
369
+ label = f"{segment}: {name}"
370
+
371
+ specs.append(MarkerSpec(
372
+ id=i,
373
+ label=label,
374
+ size_mm=base_size_mm,
375
+ ))
376
+
377
+ return generate_marker_pdf(
378
+ output_path=output_path,
379
+ marker_specs=specs,
380
+ robot_name=robot_name,
381
+ include_instructions=True,
382
+ )
383
+
384
+
385
+ def generate_quick_markers(
386
+ output_path: str = "aruco_markers.pdf",
387
+ count: int = 12,
388
+ size_mm: float = 30.0,
389
+ ) -> str:
390
+ """Quick generation of generic calibration markers.
391
+
392
+ Args:
393
+ output_path: Path to save the PDF
394
+ count: Number of markers to generate
395
+ size_mm: Size of each marker in millimeters
396
+
397
+ Returns:
398
+ Path to the generated PDF
399
+ """
400
+ return generate_marker_pdf(
401
+ output_path=output_path,
402
+ count=count,
403
+ size_mm=size_mm,
404
+ include_instructions=True,
405
+ )
406
+
407
+
408
+ # Robot-specific marker presets
409
+ # These encode expert knowledge about marker placement and sizes
410
+ ROBOT_MARKER_PRESETS = {
411
+ "mechdog-mini": {
412
+ "robot_name": "Hiwonder MechDog Mini",
413
+ "markers": [
414
+ # Body - large, visible from distance
415
+ MarkerSpec(id=0, label="Body Center", size_mm=35),
416
+ # Front Right Leg
417
+ MarkerSpec(id=1, label="FR Hip", size_mm=25),
418
+ MarkerSpec(id=2, label="FR Thigh", size_mm=25),
419
+ MarkerSpec(id=3, label="FR Shin", size_mm=20),
420
+ # Front Left Leg
421
+ MarkerSpec(id=4, label="FL Hip", size_mm=25),
422
+ MarkerSpec(id=5, label="FL Thigh", size_mm=25),
423
+ MarkerSpec(id=6, label="FL Shin", size_mm=20),
424
+ # Back Right Leg
425
+ MarkerSpec(id=7, label="BR Hip", size_mm=25),
426
+ MarkerSpec(id=8, label="BR Thigh", size_mm=25),
427
+ MarkerSpec(id=9, label="BR Shin", size_mm=20),
428
+ # Back Left Leg
429
+ MarkerSpec(id=10, label="BL Hip", size_mm=25),
430
+ MarkerSpec(id=11, label="BL Thigh", size_mm=25),
431
+ MarkerSpec(id=12, label="BL Shin", size_mm=20),
432
+ # Arm (servo 11, 12, 13 from our earlier discovery)
433
+ MarkerSpec(id=13, label="Arm Shoulder", size_mm=25),
434
+ MarkerSpec(id=14, label="Arm Elbow", size_mm=20),
435
+ MarkerSpec(id=15, label="Gripper", size_mm=15),
436
+ ],
437
+ },
438
+ "unitree-go1": {
439
+ "robot_name": "Unitree Go1",
440
+ "markers": [
441
+ MarkerSpec(id=0, label="Body Center", size_mm=60),
442
+ # Larger robot = larger markers
443
+ MarkerSpec(id=1, label="FR Hip", size_mm=40),
444
+ MarkerSpec(id=2, label="FR Thigh", size_mm=40),
445
+ MarkerSpec(id=3, label="FR Calf", size_mm=35),
446
+ MarkerSpec(id=4, label="FL Hip", size_mm=40),
447
+ MarkerSpec(id=5, label="FL Thigh", size_mm=40),
448
+ MarkerSpec(id=6, label="FL Calf", size_mm=35),
449
+ MarkerSpec(id=7, label="RR Hip", size_mm=40),
450
+ MarkerSpec(id=8, label="RR Thigh", size_mm=40),
451
+ MarkerSpec(id=9, label="RR Calf", size_mm=35),
452
+ MarkerSpec(id=10, label="RL Hip", size_mm=40),
453
+ MarkerSpec(id=11, label="RL Thigh", size_mm=40),
454
+ MarkerSpec(id=12, label="RL Calf", size_mm=35),
455
+ ],
456
+ },
457
+ "xarm-6": {
458
+ "robot_name": "xArm 6-DOF",
459
+ "markers": [
460
+ MarkerSpec(id=0, label="Base", size_mm=50),
461
+ MarkerSpec(id=1, label="Shoulder", size_mm=45),
462
+ MarkerSpec(id=2, label="Upper Arm", size_mm=40),
463
+ MarkerSpec(id=3, label="Forearm", size_mm=40),
464
+ MarkerSpec(id=4, label="Wrist 1", size_mm=35),
465
+ MarkerSpec(id=5, label="Wrist 2", size_mm=35),
466
+ MarkerSpec(id=6, label="End Effector", size_mm=30),
467
+ ],
468
+ },
469
+ "generic-quadruped": {
470
+ "robot_name": "Generic Quadruped",
471
+ "markers": [
472
+ MarkerSpec(id=0, label="Body", size_mm=40),
473
+ MarkerSpec(id=1, label="FR Segment 1", size_mm=30),
474
+ MarkerSpec(id=2, label="FR Segment 2", size_mm=25),
475
+ MarkerSpec(id=3, label="FR Segment 3", size_mm=20),
476
+ MarkerSpec(id=4, label="FL Segment 1", size_mm=30),
477
+ MarkerSpec(id=5, label="FL Segment 2", size_mm=25),
478
+ MarkerSpec(id=6, label="FL Segment 3", size_mm=20),
479
+ MarkerSpec(id=7, label="BR Segment 1", size_mm=30),
480
+ MarkerSpec(id=8, label="BR Segment 2", size_mm=25),
481
+ MarkerSpec(id=9, label="BR Segment 3", size_mm=20),
482
+ MarkerSpec(id=10, label="BL Segment 1", size_mm=30),
483
+ MarkerSpec(id=11, label="BL Segment 2", size_mm=25),
484
+ MarkerSpec(id=12, label="BL Segment 3", size_mm=20),
485
+ ],
486
+ },
487
+ "generic-arm": {
488
+ "robot_name": "Generic Robot Arm",
489
+ "markers": [
490
+ MarkerSpec(id=0, label="Base", size_mm=45),
491
+ MarkerSpec(id=1, label="Joint 1", size_mm=40),
492
+ MarkerSpec(id=2, label="Joint 2", size_mm=35),
493
+ MarkerSpec(id=3, label="Joint 3", size_mm=35),
494
+ MarkerSpec(id=4, label="Joint 4", size_mm=30),
495
+ MarkerSpec(id=5, label="Joint 5", size_mm=30),
496
+ MarkerSpec(id=6, label="End Effector", size_mm=25),
497
+ ],
498
+ },
499
+ }
500
+
501
+
502
+ def get_preset_markers(preset_name: str) -> Tuple[List[MarkerSpec], str]:
503
+ """Get marker specs for a known robot type.
504
+
505
+ Args:
506
+ preset_name: Robot preset name (e.g., "mechdog-mini", "unitree-go1")
507
+
508
+ Returns:
509
+ Tuple of (marker_specs, robot_name)
510
+
511
+ Raises:
512
+ KeyError if preset not found
513
+ """
514
+ if preset_name not in ROBOT_MARKER_PRESETS:
515
+ available = ", ".join(ROBOT_MARKER_PRESETS.keys())
516
+ raise KeyError(f"Unknown preset '{preset_name}'. Available: {available}")
517
+
518
+ preset = ROBOT_MARKER_PRESETS[preset_name]
519
+ return preset["markers"], preset["robot_name"]
520
+
521
+
522
+ def list_presets() -> List[dict]:
523
+ """List available robot presets."""
524
+ return [
525
+ {
526
+ "name": name,
527
+ "robot_name": preset["robot_name"],
528
+ "marker_count": len(preset["markers"]),
529
+ }
530
+ for name, preset in ROBOT_MARKER_PRESETS.items()
531
+ ]
532
+
533
+
534
+ # CLI integration
535
+ def add_marker_command(subparsers):
536
+ """Add the generate-markers command to the CLI."""
537
+ parser = subparsers.add_parser(
538
+ "generate-markers",
539
+ help="Generate printable ArUco markers for robot calibration",
540
+ )
541
+ parser.add_argument(
542
+ "--output", "-o",
543
+ default="aruco_markers.pdf",
544
+ help="Output PDF path (default: aruco_markers.pdf)",
545
+ )
546
+ parser.add_argument(
547
+ "--count", "-n",
548
+ type=int,
549
+ default=12,
550
+ help="Number of markers to generate (default: 12)",
551
+ )
552
+ parser.add_argument(
553
+ "--size",
554
+ type=float,
555
+ default=30.0,
556
+ help="Marker size in millimeters (default: 30)",
557
+ )
558
+ parser.add_argument(
559
+ "--robot-name",
560
+ help="Optional robot name for labeling",
561
+ )
562
+ parser.add_argument(
563
+ "--page-size",
564
+ choices=["letter", "a4"],
565
+ default="letter",
566
+ help="Page size (default: letter)",
567
+ )
568
+
569
+ return parser
570
+
571
+
572
+ def run_marker_command(args):
573
+ """Run the generate-markers command."""
574
+ from reportlab.lib.pagesizes import LETTER, A4
575
+
576
+ page_size = LETTER if args.page_size == "letter" else A4
577
+
578
+ print(f"Generating {args.count} ArUco markers...")
579
+ print(f" Size: {args.size}mm")
580
+ print(f" Output: {args.output}")
581
+
582
+ output = generate_marker_pdf(
583
+ output_path=args.output,
584
+ count=args.count,
585
+ size_mm=args.size,
586
+ page_size=page_size,
587
+ robot_name=args.robot_name,
588
+ include_instructions=True,
589
+ )
590
+
591
+ print(f"\n[OK] Markers saved to: {output}")
592
+ print("\nNext steps:")
593
+ print(" 1. Print the PDF at 100% scale")
594
+ print(" 2. Cut out markers along the dashed lines")
595
+ print(" 3. Attach to your robot's joints")
596
+ print(" 4. Run: ate robot calibrate --method aruco")
597
+
598
+
599
+ if __name__ == "__main__":
600
+ # Quick test
601
+ import sys
602
+
603
+ output = "/tmp/test_markers.pdf"
604
+ print(f"Generating test markers to {output}...")
605
+
606
+ try:
607
+ generate_quick_markers(output, count=6, size_mm=40)
608
+ print(f"Done! Open {output} to view.")
609
+ except ImportError as e:
610
+ print(f"Error: {e}")
611
+ sys.exit(1)