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,686 @@
1
+ """
2
+ Common behavior tree nodes for robotics.
3
+
4
+ These are reusable building blocks that wrap interface calls
5
+ into behavior tree-compatible actions and conditions.
6
+
7
+ The key insight: Each of these represents a SKILL that can be:
8
+ 1. Recorded as a demonstration
9
+ 2. Labeled by the community
10
+ 3. Trained into a model
11
+ 4. Deployed across robots
12
+ """
13
+
14
+ from typing import Optional, List, Any
15
+ from .tree import (
16
+ BehaviorNode,
17
+ BehaviorStatus,
18
+ Sequence,
19
+ Selector,
20
+ Repeater,
21
+ RepeatUntilFail,
22
+ Action,
23
+ Condition,
24
+ )
25
+ from ..interfaces import (
26
+ NavigationInterface,
27
+ ObjectDetectionInterface,
28
+ GripperInterface,
29
+ Vector3,
30
+ NavigationGoal,
31
+ NavigationState,
32
+ )
33
+
34
+
35
+ # =============================================================================
36
+ # Navigation Actions
37
+ # =============================================================================
38
+
39
+ class NavigateToPoint(BehaviorNode):
40
+ """
41
+ Navigate to a specific point.
42
+
43
+ Blackboard:
44
+ - Writes: "navigation_status"
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ nav: NavigationInterface,
50
+ x: float = 0,
51
+ y: float = 0,
52
+ z: float = 0,
53
+ name: str = ""
54
+ ):
55
+ super().__init__(name or f"NavigateTo({x:.1f}, {y:.1f})")
56
+ self.nav = nav
57
+ self.target = Vector3(x, y, z)
58
+ self.started = False
59
+
60
+ def tick(self) -> BehaviorStatus:
61
+ if not self.started:
62
+ result = self.nav.navigate_to_point(
63
+ self.target.x, self.target.y, self.target.z
64
+ )
65
+ if not result.success:
66
+ return BehaviorStatus.FAILURE
67
+ self.started = True
68
+
69
+ status = self.nav.get_status()
70
+ if self.blackboard:
71
+ self.blackboard.set("navigation_status", status)
72
+
73
+ if status.state == NavigationState.COMPLETED:
74
+ return BehaviorStatus.SUCCESS
75
+ elif status.state == NavigationState.FAILED:
76
+ return BehaviorStatus.FAILURE
77
+ else:
78
+ return BehaviorStatus.RUNNING
79
+
80
+ def reset(self) -> None:
81
+ super().reset()
82
+ self.started = False
83
+
84
+
85
+ class NavigateToPose(BehaviorNode):
86
+ """
87
+ Navigate to a specific pose (position + heading).
88
+
89
+ Blackboard:
90
+ - Writes: "navigation_status"
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ nav: NavigationInterface,
96
+ x: float = 0,
97
+ y: float = 0,
98
+ yaw: float = 0,
99
+ name: str = ""
100
+ ):
101
+ super().__init__(name or f"NavigateToPose({x:.1f}, {y:.1f}, {yaw:.1f})")
102
+ self.nav = nav
103
+ self.x = x
104
+ self.y = y
105
+ self.yaw = yaw
106
+ self.started = False
107
+
108
+ def tick(self) -> BehaviorStatus:
109
+ if not self.started:
110
+ result = self.nav.navigate_to_pose(self.x, self.y, self.yaw)
111
+ if not result.success:
112
+ return BehaviorStatus.FAILURE
113
+ self.started = True
114
+
115
+ status = self.nav.get_status()
116
+ if self.blackboard:
117
+ self.blackboard.set("navigation_status", status)
118
+
119
+ if status.state == NavigationState.COMPLETED:
120
+ return BehaviorStatus.SUCCESS
121
+ elif status.state == NavigationState.FAILED:
122
+ return BehaviorStatus.FAILURE
123
+ else:
124
+ return BehaviorStatus.RUNNING
125
+
126
+ def reset(self) -> None:
127
+ super().reset()
128
+ self.started = False
129
+
130
+
131
+ class Patrol(BehaviorNode):
132
+ """
133
+ Patrol a set of waypoints.
134
+
135
+ Blackboard:
136
+ - Writes: "patrol_waypoint_index", "patrol_loop_count"
137
+ """
138
+
139
+ def __init__(
140
+ self,
141
+ nav: NavigationInterface,
142
+ waypoints: List[Vector3],
143
+ loops: int = 1,
144
+ name: str = ""
145
+ ):
146
+ super().__init__(name or f"Patrol({len(waypoints)} waypoints)")
147
+ self.nav = nav
148
+ self.waypoints = waypoints
149
+ self.loops = loops
150
+ self.current_waypoint = 0
151
+ self.current_loop = 0
152
+ self.navigating = False
153
+
154
+ def tick(self) -> BehaviorStatus:
155
+ # Check if we're done
156
+ if self.loops > 0 and self.current_loop >= self.loops:
157
+ return BehaviorStatus.SUCCESS
158
+
159
+ # Start navigating to current waypoint
160
+ if not self.navigating:
161
+ wp = self.waypoints[self.current_waypoint]
162
+ result = self.nav.navigate_to_point(wp.x, wp.y, wp.z)
163
+ if not result.success:
164
+ return BehaviorStatus.FAILURE
165
+ self.navigating = True
166
+
167
+ # Check navigation status
168
+ status = self.nav.get_status()
169
+ if self.blackboard:
170
+ self.blackboard.set("patrol_waypoint_index", self.current_waypoint)
171
+ self.blackboard.set("patrol_loop_count", self.current_loop)
172
+
173
+ if status.state == NavigationState.COMPLETED:
174
+ # Move to next waypoint
175
+ self.current_waypoint += 1
176
+ self.navigating = False
177
+
178
+ if self.current_waypoint >= len(self.waypoints):
179
+ self.current_waypoint = 0
180
+ self.current_loop += 1
181
+
182
+ if self.loops > 0 and self.current_loop >= self.loops:
183
+ return BehaviorStatus.SUCCESS
184
+
185
+ return BehaviorStatus.RUNNING
186
+
187
+ elif status.state == NavigationState.FAILED:
188
+ return BehaviorStatus.FAILURE
189
+
190
+ return BehaviorStatus.RUNNING
191
+
192
+ def reset(self) -> None:
193
+ super().reset()
194
+ self.current_waypoint = 0
195
+ self.current_loop = 0
196
+ self.navigating = False
197
+
198
+
199
+ class ReturnHome(BehaviorNode):
200
+ """
201
+ Navigate to home position.
202
+
203
+ Blackboard:
204
+ - Reads: "home_position" (optional, falls back to nav.go_home())
205
+ """
206
+
207
+ def __init__(self, nav: NavigationInterface, name: str = ""):
208
+ super().__init__(name or "ReturnHome")
209
+ self.nav = nav
210
+ self.started = False
211
+
212
+ def tick(self) -> BehaviorStatus:
213
+ if not self.started:
214
+ # Try blackboard first
215
+ if self.blackboard and self.blackboard.has("home_position"):
216
+ home = self.blackboard.get("home_position")
217
+ result = self.nav.navigate_to_point(home.x, home.y, home.z)
218
+ else:
219
+ result = self.nav.go_home()
220
+
221
+ if not result.success:
222
+ return BehaviorStatus.FAILURE
223
+ self.started = True
224
+
225
+ status = self.nav.get_status()
226
+ if status.state == NavigationState.COMPLETED:
227
+ return BehaviorStatus.SUCCESS
228
+ elif status.state == NavigationState.FAILED:
229
+ return BehaviorStatus.FAILURE
230
+ return BehaviorStatus.RUNNING
231
+
232
+ def reset(self) -> None:
233
+ super().reset()
234
+ self.started = False
235
+
236
+
237
+ # =============================================================================
238
+ # Detection Actions
239
+ # =============================================================================
240
+
241
+ class DetectObject(BehaviorNode):
242
+ """
243
+ Run object detection and store results.
244
+
245
+ Blackboard:
246
+ - Writes: "detections", "detection_count"
247
+ """
248
+
249
+ def __init__(
250
+ self,
251
+ detector: ObjectDetectionInterface,
252
+ class_name: Optional[str] = None,
253
+ min_confidence: float = 0.5,
254
+ name: str = ""
255
+ ):
256
+ super().__init__(name or f"Detect({class_name or 'any'})")
257
+ self.detector = detector
258
+ self.class_name = class_name
259
+ self.min_confidence = min_confidence
260
+
261
+ def tick(self) -> BehaviorStatus:
262
+ if self.class_name:
263
+ detections = self.detector.detect_class(
264
+ self.class_name, self.min_confidence
265
+ )
266
+ else:
267
+ result = self.detector.detect()
268
+ detections = result.filter_by_confidence(self.min_confidence)
269
+
270
+ if self.blackboard:
271
+ self.blackboard.set("detections", detections)
272
+ self.blackboard.set("detection_count", len(detections))
273
+
274
+ if detections:
275
+ return BehaviorStatus.SUCCESS
276
+ return BehaviorStatus.FAILURE
277
+
278
+
279
+ class IsObjectVisible(BehaviorNode):
280
+ """
281
+ Check if an object class is visible.
282
+
283
+ This is a CONDITION - returns immediately, never RUNNING.
284
+ """
285
+
286
+ def __init__(
287
+ self,
288
+ detector: ObjectDetectionInterface,
289
+ class_name: str,
290
+ min_confidence: float = 0.5,
291
+ name: str = ""
292
+ ):
293
+ super().__init__(name or f"IsVisible({class_name})")
294
+ self.detector = detector
295
+ self.class_name = class_name
296
+ self.min_confidence = min_confidence
297
+
298
+ def tick(self) -> BehaviorStatus:
299
+ detections = self.detector.detect_class(
300
+ self.class_name, self.min_confidence
301
+ )
302
+ if detections:
303
+ return BehaviorStatus.SUCCESS
304
+ return BehaviorStatus.FAILURE
305
+
306
+
307
+ class FindNearest(BehaviorNode):
308
+ """
309
+ Find the nearest object of a class.
310
+
311
+ Blackboard:
312
+ - Writes: "target_detection", "target_position"
313
+ """
314
+
315
+ def __init__(
316
+ self,
317
+ detector: ObjectDetectionInterface,
318
+ class_name: str,
319
+ name: str = ""
320
+ ):
321
+ super().__init__(name or f"FindNearest({class_name})")
322
+ self.detector = detector
323
+ self.class_name = class_name
324
+
325
+ def tick(self) -> BehaviorStatus:
326
+ detection = self.detector.find_nearest(self.class_name)
327
+
328
+ if detection:
329
+ if self.blackboard:
330
+ self.blackboard.set("target_detection", detection)
331
+ if detection.position_3d:
332
+ self.blackboard.set("target_position", detection.position_3d)
333
+ return BehaviorStatus.SUCCESS
334
+ return BehaviorStatus.FAILURE
335
+
336
+
337
+ class ApproachObject(BehaviorNode):
338
+ """
339
+ Approach an object (navigate to it).
340
+
341
+ Blackboard:
342
+ - Reads: "target_position" or "target_detection"
343
+ """
344
+
345
+ def __init__(
346
+ self,
347
+ nav: NavigationInterface,
348
+ approach_distance: float = 0.3,
349
+ name: str = ""
350
+ ):
351
+ super().__init__(name or "ApproachObject")
352
+ self.nav = nav
353
+ self.approach_distance = approach_distance
354
+ self.started = False
355
+
356
+ def tick(self) -> BehaviorStatus:
357
+ if not self.started:
358
+ # Get target from blackboard
359
+ if not self.blackboard:
360
+ return BehaviorStatus.FAILURE
361
+
362
+ target = self.blackboard.get("target_position")
363
+ if not target:
364
+ detection = self.blackboard.get("target_detection")
365
+ if detection and detection.position_3d:
366
+ target = detection.position_3d
367
+ else:
368
+ return BehaviorStatus.FAILURE
369
+
370
+ # Navigate to target (with offset)
371
+ result = self.nav.navigate_to_point(
372
+ target.x - self.approach_distance,
373
+ target.y,
374
+ target.z
375
+ )
376
+ if not result.success:
377
+ return BehaviorStatus.FAILURE
378
+ self.started = True
379
+
380
+ status = self.nav.get_status()
381
+ if status.state == NavigationState.COMPLETED:
382
+ return BehaviorStatus.SUCCESS
383
+ elif status.state == NavigationState.FAILED:
384
+ return BehaviorStatus.FAILURE
385
+ return BehaviorStatus.RUNNING
386
+
387
+ def reset(self) -> None:
388
+ super().reset()
389
+ self.started = False
390
+
391
+
392
+ # =============================================================================
393
+ # Manipulation Actions
394
+ # =============================================================================
395
+
396
+ class PickUp(BehaviorNode):
397
+ """
398
+ Pick up an object at the target position.
399
+
400
+ Blackboard:
401
+ - Reads: "target_position"
402
+ - Writes: "has_object"
403
+ """
404
+
405
+ def __init__(self, gripper: GripperInterface, name: str = ""):
406
+ super().__init__(name or "PickUp")
407
+ self.gripper = gripper
408
+ self.state = "init"
409
+
410
+ def tick(self) -> BehaviorStatus:
411
+ if self.state == "init":
412
+ # Open gripper
413
+ result = self.gripper.open()
414
+ if not result.success:
415
+ return BehaviorStatus.FAILURE
416
+ self.state = "lowering"
417
+ return BehaviorStatus.RUNNING
418
+
419
+ elif self.state == "lowering":
420
+ # Lower body (would need body interface)
421
+ # For now, skip this step
422
+ self.state = "grasping"
423
+ return BehaviorStatus.RUNNING
424
+
425
+ elif self.state == "grasping":
426
+ result = self.gripper.grasp()
427
+ if not result.success:
428
+ return BehaviorStatus.FAILURE
429
+ self.state = "checking"
430
+ return BehaviorStatus.RUNNING
431
+
432
+ elif self.state == "checking":
433
+ # Check if we got something
434
+ status = self.gripper.get_status()
435
+ if self.blackboard:
436
+ self.blackboard.set("has_object", status.has_object)
437
+ if status.has_object:
438
+ return BehaviorStatus.SUCCESS
439
+ return BehaviorStatus.FAILURE
440
+
441
+ return BehaviorStatus.FAILURE
442
+
443
+ def reset(self) -> None:
444
+ super().reset()
445
+ self.state = "init"
446
+
447
+
448
+ class PlaceAt(BehaviorNode):
449
+ """
450
+ Place held object at a position.
451
+
452
+ Blackboard:
453
+ - Writes: "has_object" = False on success
454
+ """
455
+
456
+ def __init__(
457
+ self,
458
+ gripper: GripperInterface,
459
+ x: float,
460
+ y: float,
461
+ z: float,
462
+ name: str = ""
463
+ ):
464
+ super().__init__(name or f"PlaceAt({x:.1f}, {y:.1f}, {z:.1f})")
465
+ self.gripper = gripper
466
+ self.target = Vector3(x, y, z)
467
+ self.state = "init"
468
+
469
+ def tick(self) -> BehaviorStatus:
470
+ # Simplified - just release
471
+ if self.state == "init":
472
+ result = self.gripper.release()
473
+ if result.success:
474
+ if self.blackboard:
475
+ self.blackboard.set("has_object", False)
476
+ return BehaviorStatus.SUCCESS
477
+ return BehaviorStatus.FAILURE
478
+ return BehaviorStatus.FAILURE
479
+
480
+ def reset(self) -> None:
481
+ super().reset()
482
+ self.state = "init"
483
+
484
+
485
+ class DropInBin(BehaviorNode):
486
+ """
487
+ Drop held object into a bin.
488
+
489
+ Blackboard:
490
+ - Reads: "bin_position" (optional)
491
+ - Writes: "has_object" = False on success
492
+ """
493
+
494
+ def __init__(
495
+ self,
496
+ nav: NavigationInterface,
497
+ gripper: GripperInterface,
498
+ bin_position: Optional[Vector3] = None,
499
+ name: str = ""
500
+ ):
501
+ super().__init__(name or "DropInBin")
502
+ self.nav = nav
503
+ self.gripper = gripper
504
+ self.bin_position = bin_position
505
+ self.state = "navigate"
506
+
507
+ def tick(self) -> BehaviorStatus:
508
+ if self.state == "navigate":
509
+ # Get bin position
510
+ pos = self.bin_position
511
+ if not pos and self.blackboard:
512
+ pos = self.blackboard.get("bin_position")
513
+ if not pos:
514
+ return BehaviorStatus.FAILURE
515
+
516
+ result = self.nav.navigate_to_point(pos.x, pos.y, pos.z)
517
+ if not result.success:
518
+ return BehaviorStatus.FAILURE
519
+ self.state = "navigating"
520
+ return BehaviorStatus.RUNNING
521
+
522
+ elif self.state == "navigating":
523
+ status = self.nav.get_status()
524
+ if status.state == NavigationState.COMPLETED:
525
+ self.state = "dropping"
526
+ return BehaviorStatus.RUNNING
527
+ elif status.state == NavigationState.FAILED:
528
+ return BehaviorStatus.FAILURE
529
+ return BehaviorStatus.RUNNING
530
+
531
+ elif self.state == "dropping":
532
+ result = self.gripper.release()
533
+ if result.success:
534
+ if self.blackboard:
535
+ self.blackboard.set("has_object", False)
536
+ return BehaviorStatus.SUCCESS
537
+ return BehaviorStatus.FAILURE
538
+
539
+ return BehaviorStatus.FAILURE
540
+
541
+ def reset(self) -> None:
542
+ super().reset()
543
+ self.state = "navigate"
544
+
545
+
546
+ # =============================================================================
547
+ # Conditions
548
+ # =============================================================================
549
+
550
+ class IsBatteryLow(BehaviorNode):
551
+ """Check if battery is below threshold."""
552
+
553
+ def __init__(self, robot: Any, threshold: float = 20.0, name: str = ""):
554
+ super().__init__(name or "IsBatteryLow")
555
+ self.robot = robot
556
+ self.threshold = threshold
557
+
558
+ def tick(self) -> BehaviorStatus:
559
+ try:
560
+ status = self.robot.get_status()
561
+ if status.battery_level < self.threshold:
562
+ return BehaviorStatus.SUCCESS
563
+ except Exception:
564
+ pass
565
+ return BehaviorStatus.FAILURE
566
+
567
+
568
+ class IsPathClear(BehaviorNode):
569
+ """Check if navigation path is clear."""
570
+
571
+ def __init__(self, nav: NavigationInterface, name: str = ""):
572
+ super().__init__(name or "IsPathClear")
573
+ self.nav = nav
574
+
575
+ def tick(self) -> BehaviorStatus:
576
+ if self.nav.is_path_clear():
577
+ return BehaviorStatus.SUCCESS
578
+ return BehaviorStatus.FAILURE
579
+
580
+
581
+ class HasObject(BehaviorNode):
582
+ """Check if robot is holding an object."""
583
+
584
+ def __init__(self, gripper: GripperInterface = None, name: str = ""):
585
+ super().__init__(name or "HasObject")
586
+ self.gripper = gripper
587
+
588
+ def tick(self) -> BehaviorStatus:
589
+ # Try gripper first
590
+ if self.gripper:
591
+ status = self.gripper.get_status()
592
+ if status.has_object:
593
+ return BehaviorStatus.SUCCESS
594
+ return BehaviorStatus.FAILURE
595
+
596
+ # Fall back to blackboard
597
+ if self.blackboard and self.blackboard.get("has_object"):
598
+ return BehaviorStatus.SUCCESS
599
+ return BehaviorStatus.FAILURE
600
+
601
+
602
+ # =============================================================================
603
+ # Composite Behaviors (Higher-Level)
604
+ # =============================================================================
605
+
606
+ def PatrolAndCleanup(
607
+ nav: NavigationInterface,
608
+ detector: ObjectDetectionInterface,
609
+ gripper: GripperInterface,
610
+ waypoints: List[Vector3],
611
+ bin_position: Vector3,
612
+ target_class: str = "trash"
613
+ ) -> BehaviorNode:
614
+ """
615
+ High-level behavior: Patrol an area, pick up trash, dispose in bin.
616
+
617
+ This is the MechDog trash-picking behavior!
618
+
619
+ Tree structure:
620
+ RepeatUntilFail
621
+ └── Sequence
622
+ ├── Patrol waypoints
623
+ └── RepeatUntilFail (cleanup loop)
624
+ └── Sequence
625
+ ├── FindNearest(trash)
626
+ ├── ApproachObject
627
+ ├── PickUp
628
+ └── DropInBin
629
+ """
630
+ cleanup_loop = RepeatUntilFail(
631
+ Sequence(children=[
632
+ FindNearest(detector, target_class),
633
+ ApproachObject(nav),
634
+ PickUp(gripper),
635
+ DropInBin(nav, gripper, bin_position),
636
+ ]),
637
+ name="CleanupLoop"
638
+ )
639
+
640
+ return RepeatUntilFail(
641
+ Sequence(children=[
642
+ Patrol(nav, waypoints, loops=1),
643
+ cleanup_loop,
644
+ ]),
645
+ name="PatrolAndCleanup"
646
+ )
647
+
648
+
649
+ def SearchAndRetrieve(
650
+ nav: NavigationInterface,
651
+ detector: ObjectDetectionInterface,
652
+ gripper: GripperInterface,
653
+ target_class: str,
654
+ return_position: Vector3
655
+ ) -> BehaviorNode:
656
+ """
657
+ Search for an object, pick it up, and bring it back.
658
+
659
+ Tree structure:
660
+ Sequence
661
+ ├── Selector (find the object)
662
+ │ ├── IsObjectVisible
663
+ │ └── Sequence
664
+ │ ├── Move forward
665
+ │ └── IsObjectVisible
666
+ ├── FindNearest
667
+ ├── ApproachObject
668
+ ├── PickUp
669
+ └── NavigateToPoint (return)
670
+ """
671
+ return Sequence(
672
+ name=f"SearchAndRetrieve({target_class})",
673
+ children=[
674
+ Selector(children=[
675
+ IsObjectVisible(detector, target_class),
676
+ Sequence(children=[
677
+ NavigateToPoint(nav, 1, 0, 0, "MoveForward"),
678
+ IsObjectVisible(detector, target_class),
679
+ ]),
680
+ ]),
681
+ FindNearest(detector, target_class),
682
+ ApproachObject(nav),
683
+ PickUp(gripper),
684
+ NavigateToPoint(nav, return_position.x, return_position.y, return_position.z, "Return"),
685
+ ]
686
+ )